mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c2abf211e | ||
|
|
8ecaa4fd7a | ||
|
|
6759c26d86 | ||
|
|
76f1f140ac | ||
|
|
ca7ab5aab7 | ||
|
|
fd3530ac62 | ||
|
|
4ea863592b | ||
|
|
3f87d0336d | ||
|
|
e251482481 | ||
|
|
c79fca75f3 | ||
|
|
0b0f0b497e | ||
|
|
e9034d8da7 | ||
|
|
8fbb8b8f6e | ||
|
|
4635fdd32b | ||
|
|
6dd4382446 | ||
|
|
50acff2e81 | ||
|
|
cb6d24dfca | ||
|
|
ab4a22d8ba | ||
|
|
bc40a15de1 | ||
|
|
2542f26d96 | ||
|
|
84edb6dbda | ||
|
|
88776e7316 | ||
|
|
f669045362 | ||
|
|
68d3efbdba | ||
|
|
22d4833496 | ||
|
|
ff14caa9f7 | ||
|
|
05647135bc | ||
|
|
ae76c2a4a8 | ||
|
|
fb2b3d44fe | ||
|
|
3868557efd | ||
|
|
9bef28c8ba | ||
|
|
f25fd3b03d | ||
|
|
d1d7571ee4 | ||
|
|
6a298a068c | ||
|
|
d4aef79b68 |
No files matched your search
+2
-2
@@ -51,10 +51,10 @@ Metrics/AbcSize:
|
|||||||
- spec/**/*
|
- spec/**/*
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 1000
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 500
|
Max: 1000
|
||||||
|
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Max: 6
|
Max: 6
|
||||||
|
|||||||
+1
-1
@@ -437,7 +437,7 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.3.8)
|
rexml (3.3.9)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1 align="center" style="border-bottom: none">
|
<h1 align="center" style="border-bottom: none">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.docuseal.co">
|
<a href="https://www.docuseal.com">
|
||||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||||
<br>
|
<br>
|
||||||
</a>
|
</a>
|
||||||
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Conditional fields and formulas
|
- Conditional fields and formulas
|
||||||
- Bulk send with CSV, XLSX spreadsheet import
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||||
- [Learn more](https://www.docuseal.co/pricing)
|
- [Learn more](https://www.docuseal.com/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
|
|||||||
|
|
||||||
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.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
[Book a Meeting](https://www.docuseal.co/contact)
|
[Book a Meeting](https://www.docuseal.com/contact)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- [Signature Maker](https://www.docuseal.co/online-signature)
|
- [Signature Maker](https://www.docuseal.com/online-signature)
|
||||||
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
|
||||||
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
|
||||||
@@ -67,15 +67,18 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
submissions.each do |submission|
|
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
submission.submitters.each do |submitter|
|
submission.submitters.each do |submitter|
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -93,7 +96,10 @@ module Api
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id archived_at])
|
render json: @submission.as_json(only: %i[id archived_at])
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ module Api
|
|||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||||
|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
|
||||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ module Api
|
|||||||
|
|
||||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
|
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -104,4 +104,10 @@ class ApplicationController < ActionController::Base
|
|||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_com
|
||||||
|
return if request.domain != 'docuseal.co'
|
||||||
|
|
||||||
|
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class ErrorsController < ActionController::Base
|
class ErrorsController < ActionController::Base
|
||||||
ENTERPRISE_FEATURE_MESSAGE =
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed]
|
around_action :with_browser_locale, only: %i[show completed]
|
||||||
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -37,7 +38,10 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
if is_new_record
|
if is_new_record
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
|
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ class SubmissionsController < ApplicationController
|
|||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions.each do |submission|
|
enqueue_submission_created_webhooks(@template, submissions)
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
@@ -68,7 +66,10 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
I18n.t('submission_has_been_archived')
|
I18n.t('submission_has_been_archived')
|
||||||
end
|
end
|
||||||
@@ -85,6 +86,15 @@ class SubmissionsController < ApplicationController
|
|||||||
template.save!
|
template.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_submission_created_webhooks(template, submissions)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
|
|
||||||
TTL = 40.minutes
|
TTL = 40.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsUnarchiveController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
def create
|
||||||
|
@submission.update!(archived_at: nil)
|
||||||
|
|
||||||
|
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,8 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
|
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = submitter.submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
|
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
||||||
|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(submitter.slug)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
|
|||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
@@ -77,7 +77,7 @@ class TemplatesController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_updated_webhooks(@template)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -128,6 +128,20 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_template_created_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_template_updated_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
@@ -65,4 +65,11 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
{ files: [file] }
|
{ files: [file] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_template_created_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize!(:manage, current_user.access_token)
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
@webhook_config =
|
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
|
|
||||||
authorize!(:manage, @webhook_config)
|
authorize!(:manage, @webhook_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,37 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookPreferencesController < ApplicationController
|
class WebhookPreferencesController < ApplicationController
|
||||||
EVENTS = %w[
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
form.viewed
|
|
||||||
form.started
|
|
||||||
form.completed
|
|
||||||
form.declined
|
|
||||||
template.created
|
|
||||||
template.updated
|
|
||||||
submission.created
|
|
||||||
submission.archived
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
before_action :load_account_config
|
def update
|
||||||
authorize_resource :account_config, parent: false
|
webhook_preferences_params[:events].each do |event, val|
|
||||||
|
@webhook_url.events.delete(event) if val == '0'
|
||||||
|
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
@webhook_url.save!
|
||||||
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
|
||||||
|
|
||||||
@account_config.save!
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def webhook_preferences_params
|
||||||
@account_config =
|
params.require(:webhook_url).permit(events: {})
|
||||||
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
|
||||||
@account_config.value ||= {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_config_params
|
|
||||||
params.permit(:event, :value)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,29 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookSecretController < ApplicationController
|
class WebhookSecretController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
authorize_resource :encrypted_config, parent: false
|
|
||||||
|
|
||||||
def index; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def update
|
||||||
@encrypted_config.assign_attributes(value: {
|
@webhook_url.update!(secret: {
|
||||||
encrypted_config_params[:key] => encrypted_config_params[:value]
|
webhook_secret_params[:key] => webhook_secret_params[:value]
|
||||||
}.compact_blank)
|
}.compact_blank)
|
||||||
|
|
||||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||||
|
notice: I18n.t('webhook_secret_has_been_saved'))
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_encrypted_config
|
def webhook_secret_params
|
||||||
@encrypted_config =
|
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
|
|
||||||
end
|
|
||||||
|
|
||||||
def encrypted_config_params
|
|
||||||
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,36 +1,60 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookSettingsController < ApplicationController
|
class WebhookSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
|
||||||
authorize_resource :encrypted_config, parent: false
|
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
|
||||||
|
|
||||||
|
def index
|
||||||
|
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||||
|
@webhook_url = @webhook_urls.first_or_initialize
|
||||||
|
|
||||||
|
render @webhook_urls.size > 1 ? 'index' : 'show'
|
||||||
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
|
def new; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.assign_attributes(encrypted_config_params)
|
@webhook_url.save!
|
||||||
|
|
||||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@webhook_url.update!(update_params)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||||
|
notice: I18n.t('webhook_url_has_been_updated'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@webhook_url.destroy!
|
||||||
|
|
||||||
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend
|
||||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
if submitter.blank? || @webhook_url.blank?
|
||||||
'encrypted_config_id' => @encrypted_config.id })
|
return redirect_back(fallback_location: settings_webhooks_path,
|
||||||
|
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||||
|
end
|
||||||
|
|
||||||
|
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => @webhook_url.id)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_encrypted_config
|
def create_params
|
||||||
@encrypted_config =
|
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def encrypted_config_params
|
def update_params
|
||||||
params.require(:encrypted_config).permit(:value)
|
params.require(:webhook_url).permit(:url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
if (this.dataset.color) {
|
||||||
|
this.pad.penColor = this.dataset.color
|
||||||
|
}
|
||||||
|
|
||||||
this.pad.addEventListener('endStroke', () => {
|
this.pad.addEventListener('endStroke', () => {
|
||||||
this.updateSubmitButtonVisibility()
|
this.updateSubmitButtonVisibility()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive"
|
|
||||||
ref="scrollToElem"
|
ref="scrollToElem"
|
||||||
class="absolute"
|
class="absolute"
|
||||||
:style="{ top: scrollPadding }"
|
:style="{ top: scrollPadding }"
|
||||||
@@ -405,8 +404,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.font_size) {
|
if (this.field.preferences?.font_size) {
|
||||||
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
|
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
|
||||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
|
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.color) {
|
||||||
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
>
|
>
|
||||||
{{ t('powered_by') }}
|
{{ t('powered_by') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co/start"
|
href="https://www.docuseal.com/start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
|
|||||||
@@ -24,6 +24,44 @@
|
|||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
/>
|
/>
|
||||||
|
<Teleport
|
||||||
|
v-if="completeButtonToRef"
|
||||||
|
:to="completeButtonToRef"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
|
||||||
|
class="tooltip-left"
|
||||||
|
:class="{ tooltip: !isCompleted }"
|
||||||
|
:data-tip="t('fill_all_required_fields_to_complete')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
|
||||||
|
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
|
||||||
|
>
|
||||||
|
{{ t('complete') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
id="complete_form_button"
|
||||||
|
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
|
||||||
|
form="steps_form"
|
||||||
|
type="submit"
|
||||||
|
name="completed"
|
||||||
|
value="true"
|
||||||
|
:disabled="isSubmittingComplete"
|
||||||
|
>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isSubmittingComplete"
|
||||||
|
class="mr-1 animate-spin w-5 h-5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('complete') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Teleport>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
@@ -74,6 +112,7 @@
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
v-if="!isCompleted && !isInvite"
|
v-if="!isCompleted && !isInvite"
|
||||||
|
id="steps_form"
|
||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
@@ -592,6 +631,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
completeButtonToRef: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -772,6 +816,7 @@ export default {
|
|||||||
phoneVerifiedValues: {},
|
phoneVerifiedValues: {},
|
||||||
orientation: screen?.orientation?.type,
|
orientation: screen?.orientation?.type,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
isSubmittingComplete: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
@@ -780,6 +825,13 @@ export default {
|
|||||||
isMobile () {
|
isMobile () {
|
||||||
return /android|iphone|ipad/i.test(navigator.userAgent)
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
},
|
},
|
||||||
|
emptyValueRequiredStep () {
|
||||||
|
return this.stepFields.find((fields, index) => {
|
||||||
|
return fields.some((f) => {
|
||||||
|
return f.required && isEmpty(this.values[f.uuid])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
submitButtonText () {
|
submitButtonText () {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
return this.t('submit')
|
return this.t('submit')
|
||||||
@@ -1146,9 +1198,15 @@ export default {
|
|||||||
scrollIntoArea (area) {
|
scrollIntoArea (area) {
|
||||||
return this.$refs.areas.scrollIntoArea(area)
|
return this.$refs.areas.scrollIntoArea(area)
|
||||||
},
|
},
|
||||||
async submitStep () {
|
async submitStep (e) {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
|
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
|
||||||
|
|
||||||
|
if (forceComplete) {
|
||||||
|
this.isSubmittingComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
const submitStep = this.currentStep
|
const submitStep = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||||
@@ -1157,7 +1215,7 @@ export default {
|
|||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
if (index >= submitStep) {
|
if (forceComplete ? index === submitStep : index >= submitStep) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,7 +1225,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = submitStep === this.stepFields.length - 1
|
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
@@ -1192,7 +1250,7 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
|
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
@@ -1213,6 +1271,7 @@ export default {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
|
this.isSubmittingComplete = false
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error?.message === 'Image too small') {
|
if (error?.message === 'Image too small') {
|
||||||
@@ -1222,6 +1281,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
|
this.isSubmittingComplete = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
minimizeForm () {
|
minimizeForm () {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const en = {
|
const en = {
|
||||||
complete: 'Complete',
|
complete: 'Complete',
|
||||||
|
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
|
||||||
sign_and_complete: 'Sign and Complete',
|
sign_and_complete: 'Sign and Complete',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||||
@@ -93,6 +94,7 @@ const en = {
|
|||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
complete: 'Completar',
|
complete: 'Completar',
|
||||||
|
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
|
||||||
sign_and_complete: 'Firmar y Completar',
|
sign_and_complete: 'Firmar y Completar',
|
||||||
invite: 'Invitar',
|
invite: 'Invitar',
|
||||||
email: 'Correo electrónico',
|
email: 'Correo electrónico',
|
||||||
@@ -185,6 +187,7 @@ const es = {
|
|||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
complete: 'Completa',
|
complete: 'Completa',
|
||||||
|
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
|
||||||
sign_and_complete: 'Firma e Completa',
|
sign_and_complete: 'Firma e Completa',
|
||||||
invite: 'Invita',
|
invite: 'Invita',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
@@ -277,6 +280,7 @@ const it = {
|
|||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
complete: 'Abschließen',
|
complete: 'Abschließen',
|
||||||
|
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
|
||||||
sign_and_complete: 'Signieren und Abschließen',
|
sign_and_complete: 'Signieren und Abschließen',
|
||||||
invite: 'Einladen',
|
invite: 'Einladen',
|
||||||
email: 'E-Mail',
|
email: 'E-Mail',
|
||||||
@@ -369,6 +373,7 @@ const de = {
|
|||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
complete: 'Terminer',
|
complete: 'Terminer',
|
||||||
|
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||||
sign_and_complete: 'Signer et Terminer',
|
sign_and_complete: 'Signer et Terminer',
|
||||||
invite: 'Inviter',
|
invite: 'Inviter',
|
||||||
email: 'Courriel',
|
email: 'Courriel',
|
||||||
@@ -461,6 +466,7 @@ const fr = {
|
|||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
complete: 'Zakończ',
|
complete: 'Zakończ',
|
||||||
|
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
|
||||||
sign_and_complete: 'Podpisz i zakończ',
|
sign_and_complete: 'Podpisz i zakończ',
|
||||||
invite: 'Zaproś',
|
invite: 'Zaproś',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -553,6 +559,7 @@ const pl = {
|
|||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
complete: 'Завершити',
|
complete: 'Завершити',
|
||||||
|
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
|
||||||
sign_and_complete: 'Підписати і завершити',
|
sign_and_complete: 'Підписати і завершити',
|
||||||
invite: 'Запросити',
|
invite: 'Запросити',
|
||||||
email: 'Електронна пошта',
|
email: 'Електронна пошта',
|
||||||
@@ -645,6 +652,7 @@ const uk = {
|
|||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
complete: 'Dokončit',
|
complete: 'Dokončit',
|
||||||
|
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
||||||
sign_and_complete: 'Podepsat a dokončit',
|
sign_and_complete: 'Podepsat a dokončit',
|
||||||
invite: 'Pozvat',
|
invite: 'Pozvat',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -737,6 +745,7 @@ const cs = {
|
|||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
complete: 'Completar',
|
complete: 'Completar',
|
||||||
|
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
|
||||||
sign_and_complete: 'Assinar e Completar',
|
sign_and_complete: 'Assinar e Completar',
|
||||||
invite: 'Convidar',
|
invite: 'Convidar',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -829,6 +838,7 @@ const pt = {
|
|||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
complete: 'השלם',
|
complete: 'השלם',
|
||||||
|
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
|
||||||
sign_and_complete: 'חתום והשלם',
|
sign_and_complete: 'חתום והשלם',
|
||||||
invite: 'הזמן',
|
invite: 'הזמן',
|
||||||
email: 'דוא"ל',
|
email: 'דוא"ל',
|
||||||
@@ -922,6 +932,7 @@ const he = {
|
|||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
complete: 'Voltooien',
|
complete: 'Voltooien',
|
||||||
|
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
|
||||||
sign_and_complete: 'Ondertekenen en voltooien',
|
sign_and_complete: 'Ondertekenen en voltooien',
|
||||||
invite: 'Uitnodigen',
|
invite: 'Uitnodigen',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -1015,6 +1026,7 @@ const nl = {
|
|||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
complete: 'اكتمال',
|
complete: 'اكتمال',
|
||||||
|
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
|
||||||
sign_and_complete: 'التوقيع والاكتمال',
|
sign_and_complete: 'التوقيع والاكتمال',
|
||||||
invite: 'دعوة',
|
invite: 'دعوة',
|
||||||
email: 'البريد الإلكتروني',
|
email: 'البريد الإلكتروني',
|
||||||
@@ -1107,6 +1119,7 @@ const ar = {
|
|||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
complete: '완료',
|
complete: '완료',
|
||||||
|
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
|
||||||
sign_and_complete: '서명하고 완료하기',
|
sign_and_complete: '서명하고 완료하기',
|
||||||
invite: '초대하기',
|
invite: '초대하기',
|
||||||
email: '이메일',
|
email: '이메일',
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ export default {
|
|||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
|
if (this.field.preferences?.color) {
|
||||||
|
this.pad.penColor = this.field.preferences.color
|
||||||
|
}
|
||||||
|
|
||||||
this.pad.addEventListener('beginStroke', () => {
|
this.pad.addEventListener('beginStroke', () => {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = true
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,10 @@
|
|||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isTextSignature"
|
||||||
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
@@ -255,7 +259,7 @@
|
|||||||
class="text-base-content/60 text-xs text-center w-full mt-1"
|
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||||
>
|
>
|
||||||
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||||
href="https://www.docuseal.co/esign-disclosure"
|
href="https://www.docuseal.com/esign-disclosure"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="inline md:hidden">
|
<span class="inline md:hidden">
|
||||||
@@ -419,6 +423,10 @@ export default {
|
|||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
|
if (this.field.preferences?.color) {
|
||||||
|
this.pad.penColor = this.field.preferences.color
|
||||||
|
}
|
||||||
|
|
||||||
this.pad.addEventListener('endStroke', () => {
|
this.pad.addEventListener('endStroke', () => {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
|
:style="{ color: field.preferences?.color }"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ import Contenteditable from './contenteditable'
|
|||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
import MobileFields from './mobile_fields'
|
||||||
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@@ -558,6 +559,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defineSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -677,6 +683,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
submitterDefaultNames: FieldSubmitter.computed.names,
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
fieldsDragFieldRef: () => ref(),
|
fieldsDragFieldRef: () => ref(),
|
||||||
language () {
|
language () {
|
||||||
@@ -749,6 +756,18 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.defineSubmitters.forEach((name, index) => {
|
||||||
|
const submitter = (this.template.submitters[index] ||= {})
|
||||||
|
|
||||||
|
submitter.name = name || this.submitterDefaultNames[index]
|
||||||
|
|
||||||
|
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||||
|
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
|
||||||
|
} else {
|
||||||
|
submitter.uuid ||= v4()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co/pricing"
|
href="https://www.docuseal.com/pricing"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="link"
|
class="link"
|
||||||
>{{ t('available_in_pro') }}</a>
|
>{{ t('available_in_pro') }}</a>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
{{ t('add') }} {{ names[submitters.length] }}
|
{{ t('add') }} {{ names[lastPartyIndex] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
{{ t('add') }} {{ names[submitters.length] }}
|
{{ t('add') }} {{ names[lastPartyIndex] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -321,6 +321,15 @@ export default {
|
|||||||
this.t('twentieth_party')
|
this.t('twentieth_party')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
lastPartyIndex () {
|
||||||
|
const index = Math.max(...this.submitters.map((s) => this.names.indexOf(s.name)))
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
return index + 1
|
||||||
|
} else {
|
||||||
|
return this.submitters.length
|
||||||
|
}
|
||||||
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
return this.submitters.find((e) => e.uuid === this.modelValue)
|
return this.submitters.find((e) => e.uuid === this.modelValue)
|
||||||
}
|
}
|
||||||
@@ -353,7 +362,7 @@ export default {
|
|||||||
},
|
},
|
||||||
addSubmitter () {
|
addSubmitter () {
|
||||||
const newSubmitter = {
|
const newSubmitter = {
|
||||||
name: this.names[this.submitters.length],
|
name: this.names[this.lastPartyIndex],
|
||||||
uuid: v4()
|
uuid: v4()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
|
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co/pricing"
|
href="https://www.docuseal.com/pricing"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co/pricing"
|
href="https://www.docuseal.com/pricing"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="link"
|
class="link"
|
||||||
>{{ t('available_in_pro') }}</a>
|
>{{ t('available_in_pro') }}</a>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<select
|
<select
|
||||||
class="base-select !select-sm !h-10"
|
class="base-select !select-sm !h-10"
|
||||||
:class="{ '!text-gray-300': !mapping.column_index }"
|
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
|
||||||
required
|
required
|
||||||
@change="mapping.column_index = parseInt($event.target.value)"
|
@change="mapping.column_index = parseInt($event.target.value)"
|
||||||
>
|
>
|
||||||
@@ -285,7 +285,7 @@ export default {
|
|||||||
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
|
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid][mapping.field_name]
|
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid]?.[mapping.field_name]
|
||||||
|
|
||||||
if (fieldType && fieldType !== 'phone') {
|
if (fieldType && fieldType !== 'phone') {
|
||||||
submittersIndex[mapping.submitter_uuid].fields.push({
|
submittersIndex[mapping.submitter_uuid].fields.push({
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
<a
|
<a
|
||||||
v-if="!isConnected"
|
v-if="!isConnected"
|
||||||
class="block link text-center mt-1"
|
class="block link text-center mt-1"
|
||||||
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
|
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
data-turbo="false"
|
data-turbo="false"
|
||||||
>{{ t('learn_more') }}</a>
|
>{{ t('learn_more') }}</a>
|
||||||
|
|||||||
@@ -63,32 +63,15 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||||
webhook_config = Accounts.load_webhook_config(submitter.account)
|
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
||||||
|
|
||||||
if webhook_config
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
|
||||||
'encrypted_config_id' => webhook_config.id })
|
|
||||||
end
|
|
||||||
|
|
||||||
webhook_urls = submitter.account.webhook_urls
|
|
||||||
|
|
||||||
webhook_urls = webhook_urls.where(
|
|
||||||
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
|
|
||||||
).or(
|
|
||||||
webhook_urls.where(
|
|
||||||
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
webhook_urls.each do |webhook|
|
|
||||||
if webhook.events.include?('form.completed')
|
if webhook.events.include?('form.completed')
|
||||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
'webhook_url_id' => webhook.id })
|
'webhook_url_id' => webhook.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
if webhook.events.include?('submission.completed') && is_all_completed
|
if webhook.events.include?('submission.completed') && is_all_completed
|
||||||
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id,
|
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||||
'webhook_url_id' => webhook.id })
|
'webhook_url_id' => webhook.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,33 +5,32 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
url, secret = load_url_and_secret(submitter, params)
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed')
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**secret.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT,
|
||||||
|
**webhook_url.secret.to_h)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -45,27 +44,4 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_url_and_secret(submitter, params)
|
|
||||||
if params['encrypted_config_id']
|
|
||||||
config = EncryptedConfig.find(params['encrypted_config_id'])
|
|
||||||
|
|
||||||
url = config.value
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
|
||||||
|
|
||||||
return if preferences['form.completed'] == false
|
|
||||||
|
|
||||||
secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h
|
|
||||||
|
|
||||||
[url, secret]
|
|
||||||
elsif params['webhook_url_id']
|
|
||||||
webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id'])
|
|
||||||
|
|
||||||
webhook_url.url if webhook_url.events.include?('form.completed')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined')
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
|
||||||
|
|
||||||
return if preferences['form.declined'] == false
|
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'form.declined',
|
event_type: 'form.declined',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -43,6 +37,7 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submitter_id' => submitter.id,
|
'submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started')
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
|
||||||
|
|
||||||
return if preferences['form.started'] == false
|
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -43,6 +37,7 @@ class SendFormStartedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submitter_id' => submitter.id,
|
'submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed')
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
|
||||||
|
|
||||||
return if preferences['form.viewed'] == false
|
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -43,6 +37,7 @@ class SendFormViewedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submitter_id' => submitter.id,
|
'submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find(params['submission_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
config = Accounts.load_webhook_config(submission.account)
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submission.account)
|
|
||||||
|
|
||||||
return if preferences['submission.archived'].blank?
|
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'submission.archived',
|
event_type: 'submission.archived',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: submission.as_json(only: %i[id archived_at])
|
data: submission.as_json(only: %i[id archived_at])
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -42,6 +35,7 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submission_id' => submission.id,
|
'submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,32 +5,28 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find(params['submission_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
webhook_url = submission.account.webhook_urls.find(params['webhook_url_id'])
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
|
||||||
|
|
||||||
url = webhook_url.url if webhook_url.events.include?('submission.completed')
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'submission.completed',
|
event_type: 'submission.completed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submissions::SerializeForApi.call(submission)
|
data: Submissions::SerializeForApi.call(submission)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
|
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT,
|
||||||
|
**webhook_url.secret.to_h)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find(params['submission_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
config = Accounts.load_webhook_config(submission.account)
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(submission.account)
|
|
||||||
|
|
||||||
return if preferences['submission.created'].blank?
|
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'submission.created',
|
event_type: 'submission.created',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submissions::SerializeForApi.call(submission)
|
data: Submissions::SerializeForApi.call(submission)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -42,6 +35,7 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submission_id' => submission.id,
|
'submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find(params['template_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
config = Accounts.load_webhook_config(template.account)
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(template.account)
|
|
||||||
|
|
||||||
return if preferences['template.created'].blank?
|
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'template.created',
|
event_type: 'template.created',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Templates::SerializeForApi.call(template)
|
data: Templates::SerializeForApi.call(template)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -42,6 +35,7 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'template_id' => template.id,
|
'template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
|
|
||||||
sidekiq_options queue: :webhooks
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find(params['template_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
config = Accounts.load_webhook_config(template.account)
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
|
||||||
url = config&.value.presence
|
|
||||||
|
|
||||||
return if url.blank?
|
|
||||||
|
|
||||||
preferences = Accounts.load_webhook_preferences(template.account)
|
|
||||||
|
|
||||||
return if preferences['template.updated'].blank?
|
|
||||||
|
|
||||||
resp = begin
|
resp = begin
|
||||||
Faraday.post(url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'template.updated',
|
event_type: 'template.updated',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Templates::SerializeForApi.call(template)
|
data: Templates::SerializeForApi.call(template)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
|
**webhook_url.secret.to_h,
|
||||||
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
|
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
rescue Faraday::Error
|
rescue Faraday::Error
|
||||||
@@ -42,6 +35,7 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'template_id' => template.id,
|
'template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -138,9 +138,11 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
private
|
private
|
||||||
|
|
||||||
def build_submitter_reply_to(submitter)
|
def build_submitter_reply_to(submitter)
|
||||||
reply_to =
|
reply_to = submitter.preferences['reply_to'].presence
|
||||||
submitter.preferences['reply_to'].presence ||
|
|
||||||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
|
||||||
|
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
|
end
|
||||||
|
|
||||||
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
|
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class AccountConfig < ApplicationRecord
|
|||||||
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
||||||
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
||||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
|
|
||||||
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
||||||
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||||
|
|||||||
@@ -26,9 +26,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',
|
||||||
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
|
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
|
||||||
APP_URL_KEY = 'app_url',
|
APP_URL_KEY = 'app_url'
|
||||||
WEBHOOK_URL_KEY = 'webhook_url',
|
|
||||||
WEBHOOK_SECRET_KEY = 'webhook_secret'
|
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class UserConfig < ApplicationRecord
|
|||||||
SIGNATURE_KEY = 'signature'
|
SIGNATURE_KEY = 'signature'
|
||||||
INITIALS_KEY = 'initials'
|
INITIALS_KEY = 'initials'
|
||||||
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||||
|
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# events :text not null
|
# events :text not null
|
||||||
|
# secret :text not null
|
||||||
# sha1 :string not null
|
# sha1 :string not null
|
||||||
# url :text not null
|
# url :text not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
@@ -22,15 +23,28 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
#
|
#
|
||||||
class WebhookUrl < ApplicationRecord
|
class WebhookUrl < ApplicationRecord
|
||||||
|
EVENTS = %w[
|
||||||
|
form.viewed
|
||||||
|
form.started
|
||||||
|
form.completed
|
||||||
|
form.declined
|
||||||
|
template.created
|
||||||
|
template.updated
|
||||||
|
submission.created
|
||||||
|
submission.archived
|
||||||
|
].freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
attribute :events, :string, default: -> { [] }
|
attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
|
||||||
|
attribute :secret, :string, default: -> { {} }
|
||||||
|
|
||||||
serialize :events, coder: JSON
|
serialize :events, coder: JSON
|
||||||
|
serialize :secret, coder: JSON
|
||||||
|
|
||||||
before_validation :set_sha1
|
before_validation :set_sha1
|
||||||
|
|
||||||
encrypts :url
|
encrypts :url, :secret
|
||||||
|
|
||||||
def set_sha1
|
def set_sha1
|
||||||
self.sha1 = Digest::SHA1.hexdigest(url)
|
self.sha1 = Digest::SHA1.hexdigest(url)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<%= content_for(:canonical_url, new_user_session_url) %>
|
||||||
<div class="max-w-lg mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
|
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" x-bind:width="size" x-bind:height="size" viewBox="0 0 24 24" fill="none" stroke="currentColor" x-bind:stroke-width="stroke" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path>
|
||||||
|
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path>
|
||||||
|
<path d="M8 11v-4a4 4 0 1 1 8 0v4"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
@@ -11,6 +11,9 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<%= javascript_pack_tag 'application', defer: true %>
|
<%= javascript_pack_tag 'application', defer: true %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if canonical_url = content_for(:canonical_url) %>
|
||||||
|
<link href="<%= canonical_url %>" rel="canonical">
|
||||||
|
<% end %>
|
||||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||||
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||||
|
|||||||
@@ -27,7 +27,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 Docker 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 Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.com/install" class="link link-neutral font-bold">click</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
||||||
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
|
||||||
</a>
|
</a>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %>
|
<% if can?(:read, WebhookUrl) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% if Docuseal.multitenant? %>
|
<% if Docuseal.multitenant? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
<%= t('embedding') %>
|
<%= t('embedding') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<% align = field.dig('preferences', 'align') %>
|
<% align = field.dig('preferences', 'align') %>
|
||||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
<% color = field.dig('preferences', 'color') %>
|
||||||
|
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||||
|
|||||||
@@ -9,14 +9,18 @@
|
|||||||
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
|
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
|
<% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
|
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||||
|
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
|
||||||
|
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
|
||||||
|
<% end %>
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
|
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
|
||||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
<% if last_submitter %>
|
||||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
|
||||||
<% if is_all_completed || !is_combined_enabled %>
|
<% if is_all_completed || !is_combined_enabled %>
|
||||||
<div class="join relative">
|
<div class="join relative">
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||||
@@ -54,7 +58,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
|
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,9 +15,10 @@
|
|||||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<draw-signature data-slug="<%= params[:slug] %>" class="flex items-center h-screen p-2 justify-center">
|
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
|
||||||
|
<draw-signature data-slug="<%= params[:slug] %>" data-color="<%= field.dig('preferences', 'color') %>" class="flex items-center h-screen p-2 justify-center">
|
||||||
<%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %>
|
<%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %>
|
||||||
<input value="" type="hidden" name="values[<%= (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) }['uuid'] %>]">
|
<input value="" type="hidden" name="values[<%= field['uuid'] %>]">
|
||||||
<div class="font-semibold text-4xl text-center w-full mb-2">
|
<div class="font-semibold text-4xl text-center w-full mb-2">
|
||||||
<%= t('draw_signature') %>
|
<%= t('draw_signature') %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
</a>
|
</a>
|
||||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||||
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
</a>
|
</a>
|
||||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||||
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<%= 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: t('copy'), copied_title: t('copied') %>
|
<%= 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: t('copy'), copied_title: t('copied') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
<div class="space-y-2 md:flex-nowrap mt-2">
|
<div class="space-y-2 md:flex-nowrap mt-2">
|
||||||
<%= f.url_field :value, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
|
<%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %>
|
<%= render 'shared/turbo_modal', title: t('webhook_secret') do %>
|
||||||
<%= form_for @encrypted_config, url: webhook_secret_index_path, method: :post, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= f.fields_for :value, Struct.new(:key, :value).new(*@encrypted_config.value.to_a.first) do |ff| %>
|
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :key, class: 'label' %>
|
<%= ff.label :key, t('key'), class: 'label' %>
|
||||||
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
|
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :value, class: 'label' %>
|
<%= ff.label :value, t('value'), class: 'label' %>
|
||||||
<%= ff.text_field :value, class: 'base-input' %>
|
<%= ff.text_field :value, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<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 min-w-0">
|
||||||
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||||
|
<h1 class="text-4xl font-bold">Webhooks</h1>
|
||||||
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||||
|
<%= render 'shared/test_mode_toggle' %>
|
||||||
|
<% if @webhook_url.persisted? %>
|
||||||
|
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span><%= t('new_webhook') %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<% @webhook_urls.each do |webhook_url| %>
|
||||||
|
<%= link_to settings_webhook_path(webhook_url), class: 'card bg-base-200' do %>
|
||||||
|
<div class="card-body p-6 min-w-0">
|
||||||
|
<p class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('world', class: 'w-6 h-6 shrink-0') %>
|
||||||
|
<span class="text-xl font-semibold truncate"><%= webhook_url.url %></span>
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
|
<% webhook_url.events.each do |event| %>
|
||||||
|
<span class="badge badge-outline"><%= event %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: t('new_webhook') do %>
|
||||||
|
<%= form_for @webhook_url, url: settings_webhooks_path, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :url, 'Webhook URL', class: 'label' %>
|
||||||
|
<%= f.url_field :url, class: 'base-input', placeholder: 'https://example.com/hook', required: true %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-2">
|
||||||
|
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
|
||||||
|
<div class="flex">
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
|
||||||
|
<span>
|
||||||
|
<%= b.label %>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -1,40 +1,81 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||||
<h1 class="text-4xl font-bold">Webhooks</h1>
|
<h1 class="text-4xl font-bold">Webhook</h1>
|
||||||
<%= render 'shared/test_mode_toggle' %>
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||||
|
<% if params[:action] == 'index' %>
|
||||||
|
<%= render 'shared/test_mode_toggle' %>
|
||||||
|
<% end %>
|
||||||
|
<% if @webhook_url.persisted? && params[:action] == 'index' %>
|
||||||
|
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span><%= t('new_webhook') %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-base-200">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
|
||||||
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
<%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2">
|
<% if @webhook_url.persisted? %>
|
||||||
<%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
<div class="flex items-center space-x-2 md:absolute md:right-0">
|
||||||
|
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('lock', class: 'w-4 h-4') %>
|
||||||
|
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
|
||||||
|
<% end %>
|
||||||
|
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
|
||||||
|
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
|
<span><%= t('delete') %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= form_for @webhook_url, url: @webhook_url.persisted? ? settings_webhook_path(@webhook_url) : settings_webhooks_path, html: { autocomplete: 'off' } do |f| %>
|
||||||
|
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2">
|
||||||
|
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook', required: true %>
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
|
||||||
<a href="<%= webhook_secret_index_path %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
|
|
||||||
<%= t('add_secret') %>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% unless @webhook_url.persisted? %>
|
||||||
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
|
<div class="space-y-4 mt-4">
|
||||||
<% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-2 gap-y-2">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-2">
|
||||||
<% events.each do |event| %>
|
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
|
||||||
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
|
<div class="flex">
|
||||||
<%= f.hidden_field :event, value: event %>
|
<label class="flex items-center space-x-2">
|
||||||
<% uuid = SecureRandom.uuid %>
|
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
|
||||||
<div class="flex">
|
<span>
|
||||||
<label for="<%= uuid %>" class="flex items-center space-x-2">
|
<%= b.label %>
|
||||||
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %>
|
</span>
|
||||||
<span>
|
</label>
|
||||||
<%= event %>
|
</div>
|
||||||
</span>
|
<% end %>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if @webhook_url.persisted? %>
|
||||||
|
<%= form_for @webhook_url, url: webhook_preference_path(@webhook_url), method: :put, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-4">
|
||||||
|
<% events.each do |event| %>
|
||||||
|
<%= f.fields_for :events do |ff| %>
|
||||||
|
<div class="flex">
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()' %>
|
||||||
|
<span class="ml-2"><%= event %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,12 +84,12 @@
|
|||||||
<div class="space-y-4 mt-4">
|
<div class="space-y-4 mt-4">
|
||||||
<div class="collapse collapse-open bg-base-200 px-1">
|
<div class="collapse collapse-open bg-base-200 px-1">
|
||||||
<div class="p-4 text-xl font-medium">
|
<div class="p-4 text-xl font-medium">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center md:h-8">
|
||||||
<span>
|
<span>
|
||||||
<%= t('submission_example_payload') %>
|
<%= t('submission_example_payload') %>
|
||||||
</span>
|
</span>
|
||||||
<% if @encrypted_config.value.present? %>
|
<% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
|
||||||
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('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 %>
|
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhook_resend_path(@webhook_url), class: 'btn btn-neutral btn-outline btn-sm', method: :post %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+80
-21
@@ -20,6 +20,7 @@ en: &en
|
|||||||
language_ko: 한국어
|
language_ko: 한국어
|
||||||
hi_there: Hi there
|
hi_there: Hi there
|
||||||
thanks: Thanks
|
thanks: Thanks
|
||||||
|
unarchive: Unarchive
|
||||||
first_party: 'First Party'
|
first_party: 'First Party'
|
||||||
docuseal_trusted_signature: DocuSeal Trusted Signature
|
docuseal_trusted_signature: DocuSeal Trusted Signature
|
||||||
hello_name: Hello %{name}
|
hello_name: Hello %{name}
|
||||||
@@ -119,7 +120,7 @@ en: &en
|
|||||||
profile_details: Profile Details
|
profile_details: Profile Details
|
||||||
sign_up_with_google: Sign up with Google
|
sign_up_with_google: Sign up with Google
|
||||||
sign_up_with_microsoft: Sign up with Microsoft
|
sign_up_with_microsoft: Sign up with Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.co/terms">Terms of Service</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.com/terms">Terms of Service</a>.'
|
||||||
enter_email_to_continue: Enter email to continue
|
enter_email_to_continue: Enter email to continue
|
||||||
account: Account
|
account: Account
|
||||||
preferences: Preferences
|
preferences: Preferences
|
||||||
@@ -473,6 +474,7 @@ en: &en
|
|||||||
logo: Logo
|
logo: Logo
|
||||||
back: Back
|
back: Back
|
||||||
add_secret: Add Secret
|
add_secret: Add Secret
|
||||||
|
edit_secret: Edit Secret
|
||||||
submission_example_payload: Submission example payload
|
submission_example_payload: Submission example payload
|
||||||
there_are_no_signatures: There are no signatures
|
there_are_no_signatures: There are no signatures
|
||||||
signed_with_trusted_certificate: Signed with trusted certificate
|
signed_with_trusted_certificate: Signed with trusted certificate
|
||||||
@@ -498,6 +500,7 @@ en: &en
|
|||||||
document_template_has_been_moved: Document template has been moved.
|
document_template_has_been_moved: Document template has been moved.
|
||||||
unable_to_move_template_into_folder: Unable to move template into folder.
|
unable_to_move_template_into_folder: Unable to move template into folder.
|
||||||
template_has_been_unarchived: Template has been unarchived.
|
template_has_been_unarchived: Template has been unarchived.
|
||||||
|
submission_has_been_unarchived: Submission has been unarchived.
|
||||||
unable_to_update_file: Unable to upload file.
|
unable_to_update_file: Unable to upload file.
|
||||||
user_has_been_invited: User has been invited.
|
user_has_been_invited: User has been invited.
|
||||||
unable_to_update_user: Unable to update user.
|
unable_to_update_user: Unable to update user.
|
||||||
@@ -508,13 +511,17 @@ en: &en
|
|||||||
webhook_secret_has_been_saved: Webhook Secret has been saved.
|
webhook_secret_has_been_saved: Webhook Secret has been saved.
|
||||||
webhook_url_has_been_saved: Webhook URL has been saved.
|
webhook_url_has_been_saved: Webhook URL has been saved.
|
||||||
webhook_request_has_been_sent: Webhook request has been sent.
|
webhook_request_has_been_sent: Webhook request has been sent.
|
||||||
|
webhook_url_has_been_updated: Webhook URL has been updated.
|
||||||
|
webhook_url_has_been_deleted: Webhook URL has been deleted.
|
||||||
|
unable_to_resend_webhook_request: Unable to resend webhook request.
|
||||||
|
new_webhook: New Webhook
|
||||||
|
delete_webhook: Delete webhook
|
||||||
count_submissions_have_been_created: '%{count} submissions have been created.'
|
count_submissions_have_been_created: '%{count} submissions have been created.'
|
||||||
gmail_has_been_connected: Gmail has been connected
|
gmail_has_been_connected: Gmail has been connected
|
||||||
microsoft_account_has_been_connected: Microsoft Account has been connected
|
microsoft_account_has_been_connected: Microsoft Account has been connected
|
||||||
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
|
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
|
||||||
connected_successfully: Connected successfully.
|
connected_successfully: Connected successfully.
|
||||||
user_nameid_not_found: 'User %{nameid} not found.'
|
user_nameid_not_found: 'User %{nameid} not found.'
|
||||||
webhook_request_has_been_sent: Webhook request has been sent.
|
|
||||||
sso_settings_have_been_updated: SSO settings have been updated.
|
sso_settings_have_been_updated: SSO settings have been updated.
|
||||||
sms_has_been_sent: SMS has been sent.
|
sms_has_been_sent: SMS has been sent.
|
||||||
account_has_been_created: Account has been created.
|
account_has_been_created: Account has been created.
|
||||||
@@ -621,7 +628,9 @@ en: &en
|
|||||||
archived_users: Archived Users
|
archived_users: Archived Users
|
||||||
embedding_users: Embedding Users
|
embedding_users: Embedding Users
|
||||||
view_embedding_users: View Embedding Users
|
view_embedding_users: View Embedding Users
|
||||||
view_users: View Users
|
key: Key
|
||||||
|
value: Value
|
||||||
|
webhook_secret: Webhook Secret
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||||
@@ -658,6 +667,7 @@ en: &en
|
|||||||
read: Read your data
|
read: Read your data
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
unarchive: Desarchivar
|
||||||
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
|
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
|
||||||
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
|
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
|
||||||
first_party: 'Primera Parte'
|
first_party: 'Primera Parte'
|
||||||
@@ -759,7 +769,7 @@ es: &es
|
|||||||
profile_details: Detalles del perfil
|
profile_details: Detalles del perfil
|
||||||
sign_up_with_google: Registrarse con Google
|
sign_up_with_google: Registrarse con Google
|
||||||
sign_up_with_microsoft: Registrarse con Microsoft
|
sign_up_with_microsoft: Registrarse con Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.co/terms">Términos de Servicio</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.com/terms">Términos de Servicio</a>.'
|
||||||
enter_email_to_continue: Ingresa tu correo electrónico para continuar
|
enter_email_to_continue: Ingresa tu correo electrónico para continuar
|
||||||
account: Cuenta
|
account: Cuenta
|
||||||
preferences: Preferencias
|
preferences: Preferencias
|
||||||
@@ -1113,6 +1123,7 @@ es: &es
|
|||||||
logo: Logotipo
|
logo: Logotipo
|
||||||
back: Atrás
|
back: Atrás
|
||||||
add_secret: Agregar secreto
|
add_secret: Agregar secreto
|
||||||
|
edit_secret: Editar secreto
|
||||||
submission_example_payload: Ejemplo de payload de envío
|
submission_example_payload: Ejemplo de payload de envío
|
||||||
there_are_no_signatures: No hay firmas
|
there_are_no_signatures: No hay firmas
|
||||||
signed_with_trusted_certificate: Firmado con certificado de confianza
|
signed_with_trusted_certificate: Firmado con certificado de confianza
|
||||||
@@ -1138,6 +1149,7 @@ es: &es
|
|||||||
document_template_has_been_moved: La plantilla de documento ha sido movida.
|
document_template_has_been_moved: La plantilla de documento ha sido movida.
|
||||||
unable_to_move_template_into_folder: No se pudo mover la plantilla a la carpeta.
|
unable_to_move_template_into_folder: No se pudo mover la plantilla a la carpeta.
|
||||||
template_has_been_unarchived: La plantilla ha sido desarchivada.
|
template_has_been_unarchived: La plantilla ha sido desarchivada.
|
||||||
|
submission_has_been_unarchived: La presentación ha sido desarchivada.
|
||||||
unable_to_update_file: No se pudo subir el archivo.
|
unable_to_update_file: No se pudo subir el archivo.
|
||||||
user_has_been_invited: El usuario ha sido invitado.
|
user_has_been_invited: El usuario ha sido invitado.
|
||||||
unable_to_update_user: No se pudo actualizar el usuario.
|
unable_to_update_user: No se pudo actualizar el usuario.
|
||||||
@@ -1148,13 +1160,17 @@ es: &es
|
|||||||
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
|
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
|
||||||
webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
|
webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
|
||||||
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
|
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
|
||||||
|
webhook_url_has_been_updated: La URL del Webhook ha sido actualizada.
|
||||||
|
webhook_url_has_been_deleted: La URL del Webhook ha sido eliminada.
|
||||||
|
unable_to_resend_webhook_request: No se pudo reenviar la solicitud del webhook.
|
||||||
|
new_webhook: Nuevo Webhook
|
||||||
|
delete_webhook: Eliminar webhook
|
||||||
count_submissions_have_been_created: '%{count} envíos han sido creados.'
|
count_submissions_have_been_created: '%{count} envíos han sido creados.'
|
||||||
gmail_has_been_connected: Gmail ha sido conectado.
|
gmail_has_been_connected: Gmail ha sido conectado.
|
||||||
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
|
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
|
||||||
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
|
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
|
||||||
connected_successfully: Conectado con éxito.
|
connected_successfully: Conectado con éxito.
|
||||||
user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
|
user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
|
||||||
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
|
|
||||||
sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
|
sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
|
||||||
sms_has_been_sent: El SMS ha sido enviado.
|
sms_has_been_sent: El SMS ha sido enviado.
|
||||||
account_has_been_created: La cuenta ha sido creada.
|
account_has_been_created: La cuenta ha sido creada.
|
||||||
@@ -1261,6 +1277,9 @@ es: &es
|
|||||||
archived_users: Usuarios Archivados
|
archived_users: Usuarios Archivados
|
||||||
embedding_users: Usuarios Integrados
|
embedding_users: Usuarios Integrados
|
||||||
view_embedding_users: Ver Usuarios Integrado
|
view_embedding_users: Ver Usuarios Integrado
|
||||||
|
key: Clave
|
||||||
|
value: Valor
|
||||||
|
webhook_secret: Secreto del Webhook
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||||
@@ -1297,6 +1316,7 @@ es: &es
|
|||||||
read: Leer tus datos
|
read: Leer tus datos
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
unarchive: Ripristina
|
||||||
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
|
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
|
||||||
enforce_recipients_order: Aplicar el orden de los destinatarios
|
enforce_recipients_order: Aplicar el orden de los destinatarios
|
||||||
first_party: 'Prima parte'
|
first_party: 'Prima parte'
|
||||||
@@ -1398,7 +1418,7 @@ it: &it
|
|||||||
profile_details: Dettagli del profilo
|
profile_details: Dettagli del profilo
|
||||||
sign_up_with_google: Registrati con Google
|
sign_up_with_google: Registrati con Google
|
||||||
sign_up_with_microsoft: Registrati con Microsoft
|
sign_up_with_microsoft: Registrati con Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.co/terms">Termini di servizio</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.com/terms">Termini di servizio</a>.'
|
||||||
enter_email_to_continue: "Inserisci l'email per continuare"
|
enter_email_to_continue: "Inserisci l'email per continuare"
|
||||||
account: Account
|
account: Account
|
||||||
preferences: Preferenze
|
preferences: Preferenze
|
||||||
@@ -1752,6 +1772,7 @@ it: &it
|
|||||||
logo: Logo
|
logo: Logo
|
||||||
back: Indietro
|
back: Indietro
|
||||||
add_secret: Aggiungi segreto
|
add_secret: Aggiungi segreto
|
||||||
|
edit_secret: Modifica segreto
|
||||||
submission_example_payload: Esempio di payload di invio
|
submission_example_payload: Esempio di payload di invio
|
||||||
there_are_no_signatures: Non ci sono firme
|
there_are_no_signatures: Non ci sono firme
|
||||||
signed_with_trusted_certificate: Firmato con certificato affidabile
|
signed_with_trusted_certificate: Firmato con certificato affidabile
|
||||||
@@ -1777,6 +1798,7 @@ it: &it
|
|||||||
document_template_has_been_moved: Il modello di documento è stato spostato.
|
document_template_has_been_moved: Il modello di documento è stato spostato.
|
||||||
unable_to_move_template_into_folder: Impossibile spostare il modello nella cartella.
|
unable_to_move_template_into_folder: Impossibile spostare il modello nella cartella.
|
||||||
template_has_been_unarchived: Il modello è stato disarchiviato.
|
template_has_been_unarchived: Il modello è stato disarchiviato.
|
||||||
|
submission_has_been_unarchived: La sottomissione è stata dearchiviata.
|
||||||
unable_to_update_file: Impossibile caricare il file.
|
unable_to_update_file: Impossibile caricare il file.
|
||||||
user_has_been_invited: "L'utente è stato invitato."
|
user_has_been_invited: "L'utente è stato invitato."
|
||||||
unable_to_update_user: Impossibile aggiornare l'utente.
|
unable_to_update_user: Impossibile aggiornare l'utente.
|
||||||
@@ -1787,13 +1809,17 @@ it: &it
|
|||||||
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
|
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
|
||||||
webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
|
webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
|
||||||
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
|
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
|
||||||
|
webhook_url_has_been_updated: "L'URL del Webhook è stata aggiornata."
|
||||||
|
webhook_url_has_been_deleted: "L'URL del Webhook è stata eliminata."
|
||||||
|
unable_to_resend_webhook_request: Impossibile reinviare la richiesta del webhook.
|
||||||
|
new_webhook: Nuovo Webhook
|
||||||
|
delete_webhook: Elimina webhook
|
||||||
count_submissions_have_been_created: '%{count} invii sono stati creati.'
|
count_submissions_have_been_created: '%{count} invii sono stati creati.'
|
||||||
gmail_has_been_connected: Gmail è stato connesso.
|
gmail_has_been_connected: Gmail è stato connesso.
|
||||||
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
|
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
|
||||||
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
|
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
|
||||||
connected_successfully: Collegamento avvenuto con successo.
|
connected_successfully: Collegamento avvenuto con successo.
|
||||||
user_nameid_not_found: 'Utente %{nameid} non trovato.'
|
user_nameid_not_found: 'Utente %{nameid} non trovato.'
|
||||||
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
|
|
||||||
sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
|
sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
|
||||||
sms_has_been_sent: "L'SMS è stato inviato."
|
sms_has_been_sent: "L'SMS è stato inviato."
|
||||||
account_has_been_created: "L'account è stato creato."
|
account_has_been_created: "L'account è stato creato."
|
||||||
@@ -1900,6 +1926,9 @@ it: &it
|
|||||||
archived_users: Utenti Archiviati
|
archived_users: Utenti Archiviati
|
||||||
embedding_users: Utenti Incorporati
|
embedding_users: Utenti Incorporati
|
||||||
view_embedding_users: Visualizza Utenti Incorporati
|
view_embedding_users: Visualizza Utenti Incorporati
|
||||||
|
key: Chiave
|
||||||
|
value: Valore
|
||||||
|
webhook_secret: Segreto del Webhook
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||||
@@ -1936,6 +1965,7 @@ it: &it
|
|||||||
read: Leggi i tuoi dati
|
read: Leggi i tuoi dati
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
unarchive: Désarchiver
|
||||||
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
|
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
|
||||||
enforce_recipients_order: "Respecter l'ordre des destinataires"
|
enforce_recipients_order: "Respecter l'ordre des destinataires"
|
||||||
first_party: 'Première partie'
|
first_party: 'Première partie'
|
||||||
@@ -2038,7 +2068,7 @@ fr: &fr
|
|||||||
sign_up_with_google: S'inscrire avec Google
|
sign_up_with_google: S'inscrire avec Google
|
||||||
sign_up_with_microsoft: S'inscrire avec Microsoft
|
sign_up_with_microsoft: S'inscrire avec Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: |
|
by_creating_an_account_you_agree_to_our_html: |
|
||||||
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.co/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.co/terms">Conditions d'Utilisation</a>.
|
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.com/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.com/terms">Conditions d'Utilisation</a>.
|
||||||
enter_email_to_continue: Entrez votre e-mail pour continuer
|
enter_email_to_continue: Entrez votre e-mail pour continuer
|
||||||
account: Compte
|
account: Compte
|
||||||
preferences: Préférences
|
preferences: Préférences
|
||||||
@@ -2392,6 +2422,7 @@ fr: &fr
|
|||||||
logo: Logo
|
logo: Logo
|
||||||
back: Retour
|
back: Retour
|
||||||
add_secret: Ajouter un secret
|
add_secret: Ajouter un secret
|
||||||
|
edit_secret: Modifier le Secret
|
||||||
submission_example_payload: Exemple de payload de soumission
|
submission_example_payload: Exemple de payload de soumission
|
||||||
there_are_no_signatures: "Il n'y a pas de signatures"
|
there_are_no_signatures: "Il n'y a pas de signatures"
|
||||||
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
||||||
@@ -2417,6 +2448,7 @@ fr: &fr
|
|||||||
document_template_has_been_moved: Le modèle de document a été déplacé.
|
document_template_has_been_moved: Le modèle de document a été déplacé.
|
||||||
unable_to_move_template_into_folder: Impossible de déplacer le modèle dans le dossier.
|
unable_to_move_template_into_folder: Impossible de déplacer le modèle dans le dossier.
|
||||||
template_has_been_unarchived: Le modèle a été désarchivé.
|
template_has_been_unarchived: Le modèle a été désarchivé.
|
||||||
|
submission_has_been_unarchived: La soumission a été désarchivée.
|
||||||
unable_to_update_file: Impossible de télécharger le fichier.
|
unable_to_update_file: Impossible de télécharger le fichier.
|
||||||
user_has_been_updated: "L'utilisateur a été mis à jour."
|
user_has_been_updated: "L'utilisateur a été mis à jour."
|
||||||
unable_to_remove_user: Impossible de supprimer l'utilisateur.
|
unable_to_remove_user: Impossible de supprimer l'utilisateur.
|
||||||
@@ -2427,13 +2459,17 @@ fr: &fr
|
|||||||
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
|
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
|
||||||
webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
|
webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
|
||||||
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
|
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
|
||||||
|
webhook_url_has_been_updated: "L'URL du Webhook a été mise à jour."
|
||||||
|
webhook_url_has_been_deleted: "L'URL du Webhook a été supprimée."
|
||||||
|
unable_to_resend_webhook_request: Impossible de renvoyer la requête du webhook.
|
||||||
|
new_webhook: Nouveau Webhook
|
||||||
|
delete_webhook: Supprimer le webhook
|
||||||
count_submissions_have_been_created: '%{count} soumissions ont été créées.'
|
count_submissions_have_been_created: '%{count} soumissions ont été créées.'
|
||||||
gmail_has_been_connected: Gmail a été connecté.
|
gmail_has_been_connected: Gmail a été connecté.
|
||||||
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
|
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
|
||||||
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
|
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
|
||||||
connected_successfully: Connecté avec succès.
|
connected_successfully: Connecté avec succès.
|
||||||
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
|
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
|
||||||
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
|
|
||||||
sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
|
sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
|
||||||
sms_has_been_sent: Le SMS a été envoyé.
|
sms_has_been_sent: Le SMS a été envoyé.
|
||||||
account_has_been_created: Le compte a été créé.
|
account_has_been_created: Le compte a été créé.
|
||||||
@@ -2540,6 +2576,9 @@ fr: &fr
|
|||||||
archived_users: Utilisateurs Archivés
|
archived_users: Utilisateurs Archivés
|
||||||
embedding_users: Utilisateurs Intégrés
|
embedding_users: Utilisateurs Intégrés
|
||||||
view_embedding_users: Voir les Utilisateurs Intégrés
|
view_embedding_users: Voir les Utilisateurs Intégrés
|
||||||
|
key: Clé
|
||||||
|
value: Valeur
|
||||||
|
webhook_secret: Secret du Webhook
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||||
@@ -2576,6 +2615,7 @@ fr: &fr
|
|||||||
read: Lire vos données
|
read: Lire vos données
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
unarchive: Desarquivar
|
||||||
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
|
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
|
||||||
enforce_recipients_order: 'Forçar a ordem dos recipientes'
|
enforce_recipients_order: 'Forçar a ordem dos recipientes'
|
||||||
first_party: 'Primeira Parte'
|
first_party: 'Primeira Parte'
|
||||||
@@ -2677,7 +2717,7 @@ pt: &pt
|
|||||||
profile_details: Detalhes do perfil
|
profile_details: Detalhes do perfil
|
||||||
sign_up_with_google: Inscrever-se com Google
|
sign_up_with_google: Inscrever-se com Google
|
||||||
sign_up_with_microsoft: Inscrever-se com Microsoft
|
sign_up_with_microsoft: Inscrever-se com Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.co/terms">Termos de Serviço</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.com/terms">Termos de Serviço</a>.'
|
||||||
enter_email_to_continue: Insira o e-mail para continuar
|
enter_email_to_continue: Insira o e-mail para continuar
|
||||||
account: Conta
|
account: Conta
|
||||||
preferences: Preferências
|
preferences: Preferências
|
||||||
@@ -3031,6 +3071,7 @@ pt: &pt
|
|||||||
logo: Logotipo
|
logo: Logotipo
|
||||||
back: Voltar
|
back: Voltar
|
||||||
add_secret: Adicionar segredo
|
add_secret: Adicionar segredo
|
||||||
|
edit_secret: Editar Segredo
|
||||||
submission_example_payload: Exemplo de payload de submissão
|
submission_example_payload: Exemplo de payload de submissão
|
||||||
there_are_no_signatures: Não há assinaturas
|
there_are_no_signatures: Não há assinaturas
|
||||||
signed_with_trusted_certificate: Assinado com certificado confiável
|
signed_with_trusted_certificate: Assinado com certificado confiável
|
||||||
@@ -3056,6 +3097,7 @@ pt: &pt
|
|||||||
document_template_has_been_moved: O modelo de documento foi movido.
|
document_template_has_been_moved: O modelo de documento foi movido.
|
||||||
unable_to_move_template_into_folder: Não foi possível mover o modelo para a pasta.
|
unable_to_move_template_into_folder: Não foi possível mover o modelo para a pasta.
|
||||||
template_has_been_unarchived: O modelo foi desarquivado.
|
template_has_been_unarchived: O modelo foi desarquivado.
|
||||||
|
submission_has_been_unarchived: A submissao foi desarquivada.
|
||||||
unable_to_update_file: Não foi possível atualizar o arquivo.
|
unable_to_update_file: Não foi possível atualizar o arquivo.
|
||||||
user_has_been_updated: O usuário foi atualizado.
|
user_has_been_updated: O usuário foi atualizado.
|
||||||
unable_to_remove_user: Não foi possível remover o usuário.
|
unable_to_remove_user: Não foi possível remover o usuário.
|
||||||
@@ -3066,13 +3108,17 @@ pt: &pt
|
|||||||
webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
|
webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
|
||||||
webhook_url_has_been_saved: A URL do Webhook foi salva.
|
webhook_url_has_been_saved: A URL do Webhook foi salva.
|
||||||
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
|
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
|
||||||
|
webhook_url_has_been_updated: URL do Webhook foi atualizada.
|
||||||
|
webhook_url_has_been_deleted: URL do Webhook foi excluída.
|
||||||
|
unable_to_resend_webhook_request: Não foi possível reenviar a solicitação do webhook.
|
||||||
|
new_webhook: Novo Webhook
|
||||||
|
delete_webhook: Excluir webhook
|
||||||
count_submissions_have_been_created: '%{count} submissões foram criadas.'
|
count_submissions_have_been_created: '%{count} submissões foram criadas.'
|
||||||
gmail_has_been_connected: O Gmail foi conectado
|
gmail_has_been_connected: O Gmail foi conectado
|
||||||
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
|
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
|
||||||
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
|
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
|
||||||
connected_successfully: Conectado com sucesso.
|
connected_successfully: Conectado com sucesso.
|
||||||
user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
|
user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
|
||||||
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
|
|
||||||
sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
|
sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
|
||||||
sms_has_been_sent: O SMS foi enviado.
|
sms_has_been_sent: O SMS foi enviado.
|
||||||
account_has_been_created: A conta foi criada.
|
account_has_been_created: A conta foi criada.
|
||||||
@@ -3179,6 +3225,9 @@ pt: &pt
|
|||||||
archived_users: Usuários Arquivados
|
archived_users: Usuários Arquivados
|
||||||
embedding_users: Usuários Incorporados
|
embedding_users: Usuários Incorporados
|
||||||
view_embedding_users: Ver Usuários Incorporados
|
view_embedding_users: Ver Usuários Incorporados
|
||||||
|
key: Chave
|
||||||
|
value: Valor
|
||||||
|
webhook_secret: Segredo do Webhook
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||||
@@ -3215,6 +3264,7 @@ pt: &pt
|
|||||||
read: Ler seus dados
|
read: Ler seus dados
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
unarchive: Wiederherstellen
|
||||||
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
|
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
|
||||||
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
|
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
|
||||||
first_party: 'Erste Partei'
|
first_party: 'Erste Partei'
|
||||||
@@ -3316,7 +3366,7 @@ de: &de
|
|||||||
profile_details: Profildetails
|
profile_details: Profildetails
|
||||||
sign_up_with_google: Mit Google registrieren
|
sign_up_with_google: Mit Google registrieren
|
||||||
sign_up_with_microsoft: Mit Microsoft registrieren
|
sign_up_with_microsoft: Mit Microsoft registrieren
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.co/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.co/terms">Nutzungsbedingungen</a> zu.'
|
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.com/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.com/terms">Nutzungsbedingungen</a> zu.'
|
||||||
enter_email_to_continue: E-Mail eingeben, um fortzufahren
|
enter_email_to_continue: E-Mail eingeben, um fortzufahren
|
||||||
account: Konto
|
account: Konto
|
||||||
preferences: Einstellungen
|
preferences: Einstellungen
|
||||||
@@ -3669,7 +3719,8 @@ de: &de
|
|||||||
api_key: API-Schlüssel
|
api_key: API-Schlüssel
|
||||||
logo: Logo
|
logo: Logo
|
||||||
back: Zurück
|
back: Zurück
|
||||||
add_secret: Geheimnis hinzufügen
|
add_secret: Geheimnis hinzuf
|
||||||
|
edit_secret: Geheimnis bearb
|
||||||
submission_example_payload: Beispiel-Payload für Einreichung
|
submission_example_payload: Beispiel-Payload für Einreichung
|
||||||
there_are_no_signatures: Es gibt keine Unterschriften
|
there_are_no_signatures: Es gibt keine Unterschriften
|
||||||
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
||||||
@@ -3695,6 +3746,7 @@ de: &de
|
|||||||
document_template_has_been_moved: Die Dokumentvorlage wurde verschoben.
|
document_template_has_been_moved: Die Dokumentvorlage wurde verschoben.
|
||||||
unable_to_move_template_into_folder: Vorlage konnte nicht in den Ordner verschoben werden.
|
unable_to_move_template_into_folder: Vorlage konnte nicht in den Ordner verschoben werden.
|
||||||
template_has_been_unarchived: Die Vorlage wurde entarchiviert.
|
template_has_been_unarchived: Die Vorlage wurde entarchiviert.
|
||||||
|
submission_has_been_unarchived: Die Einreichung wurde wiederhergestellt.
|
||||||
unable_to_update_file: Datei konnte nicht hochgeladen werden.
|
unable_to_update_file: Datei konnte nicht hochgeladen werden.
|
||||||
user_has_been_updated: Der Benutzer wurde aktualisiert.
|
user_has_been_updated: Der Benutzer wurde aktualisiert.
|
||||||
unable_to_remove_user: Benutzer konnte nicht entfernt werden.
|
unable_to_remove_user: Benutzer konnte nicht entfernt werden.
|
||||||
@@ -3705,13 +3757,17 @@ de: &de
|
|||||||
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
|
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
|
||||||
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
|
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
|
||||||
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
|
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
|
||||||
|
webhook_url_has_been_updated: Webhook-URL wurde aktualisiert.
|
||||||
|
webhook_url_has_been_deleted: Webhook-URL wurde gelöscht.
|
||||||
|
unable_to_resend_webhook_request: Webhook-Anfrage konnte nicht erneut gesendet werden.
|
||||||
|
new_webhook: Neuer Webhook
|
||||||
|
delete_webhook: Webhook löschen
|
||||||
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
|
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
|
||||||
gmail_has_been_connected: Gmail wurde verbunden.
|
gmail_has_been_connected: Gmail wurde verbunden.
|
||||||
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
|
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
|
||||||
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
|
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
|
||||||
connected_successfully: Erfolgreich verbunden.
|
connected_successfully: Erfolgreich verbunden.
|
||||||
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
|
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
|
||||||
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
|
|
||||||
sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
|
sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
|
||||||
sms_has_been_sent: Die SMS wurde gesendet.
|
sms_has_been_sent: Die SMS wurde gesendet.
|
||||||
account_has_been_created: Das Konto wurde erstellt.
|
account_has_been_created: Das Konto wurde erstellt.
|
||||||
@@ -3818,6 +3874,9 @@ de: &de
|
|||||||
archived_users: Archivierte Benutzer
|
archived_users: Archivierte Benutzer
|
||||||
embedding_users: Einbettende Benutzer
|
embedding_users: Einbettende Benutzer
|
||||||
view_embedding_users: Einbettende Benutzer anzeigen
|
view_embedding_users: Einbettende Benutzer anzeigen
|
||||||
|
key: Schlüssel
|
||||||
|
value: Wert
|
||||||
|
webhook_secret: Webhook-Geheimnis
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||||
@@ -3904,7 +3963,7 @@ pl:
|
|||||||
profile_details: Szczegóły profilu
|
profile_details: Szczegóły profilu
|
||||||
sign_up_with_google: Zarejestruj się przez Google
|
sign_up_with_google: Zarejestruj się przez Google
|
||||||
sign_up_with_microsoft: Zarejestruj się przez Microsoft
|
sign_up_with_microsoft: Zarejestruj się przez Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.co/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.co/terms">Regulamin</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.'
|
||||||
enter_email_to_continue: Wprowadź e-mail, aby kontynuować
|
enter_email_to_continue: Wprowadź e-mail, aby kontynuować
|
||||||
|
|
||||||
uk:
|
uk:
|
||||||
@@ -3958,7 +4017,7 @@ uk:
|
|||||||
profile_details: Дані профілю
|
profile_details: Дані профілю
|
||||||
sign_up_with_google: Зареєструватися через Google
|
sign_up_with_google: Зареєструватися через Google
|
||||||
sign_up_with_microsoft: Зареєструватися через Microsoft
|
sign_up_with_microsoft: Зареєструватися через Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.co/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.co/terms">Умовами надання послуг</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.'
|
||||||
enter_email_to_continue: Введіть електронну пошту, щоб продовжити
|
enter_email_to_continue: Введіть електронну пошту, щоб продовжити
|
||||||
|
|
||||||
cs:
|
cs:
|
||||||
@@ -4012,7 +4071,7 @@ cs:
|
|||||||
profile_details: Detaily profilu
|
profile_details: Detaily profilu
|
||||||
sign_up_with_google: Zaregistrovat se pomocí Googlu
|
sign_up_with_google: Zaregistrovat se pomocí Googlu
|
||||||
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
|
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.co/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.co/terms">Podmínkami služby</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.'
|
||||||
enter_email_to_continue: Zadejte e-mail pro pokračování
|
enter_email_to_continue: Zadejte e-mail pro pokračování
|
||||||
|
|
||||||
he:
|
he:
|
||||||
@@ -4066,7 +4125,7 @@ he:
|
|||||||
profile_details: פרטי הפרופיל
|
profile_details: פרטי הפרופיל
|
||||||
sign_up_with_google: הירשם עם גוגל
|
sign_up_with_google: הירשם עם גוגל
|
||||||
sign_up_with_microsoft: הירשם עם מיקרוסופט
|
sign_up_with_microsoft: הירשם עם מיקרוסופט
|
||||||
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.co/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.co/terms">תנאי השירות</a> שלנו.'
|
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.'
|
||||||
enter_email_to_continue: הכנס דוא"ל כדי להמשיך
|
enter_email_to_continue: הכנס דוא"ל כדי להמשיך
|
||||||
|
|
||||||
nl:
|
nl:
|
||||||
@@ -4120,7 +4179,7 @@ nl:
|
|||||||
profile_details: Profielgegevens
|
profile_details: Profielgegevens
|
||||||
sign_up_with_google: Aanmelden met Google
|
sign_up_with_google: Aanmelden met Google
|
||||||
sign_up_with_microsoft: Aanmelden met Microsoft
|
sign_up_with_microsoft: Aanmelden met Microsoft
|
||||||
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.co/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.co/terms">Gebruiksvoorwaarden</a>.'
|
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Gebruiksvoorwaarden</a>.'
|
||||||
enter_email_to_continue: Voer e-mail in om door te gaan
|
enter_email_to_continue: Voer e-mail in om door te gaan
|
||||||
|
|
||||||
ar:
|
ar:
|
||||||
@@ -4174,7 +4233,7 @@ ar:
|
|||||||
profile_details: تفاصيل الملف الشخصي
|
profile_details: تفاصيل الملف الشخصي
|
||||||
sign_up_with_google: الاشتراك باستخدام جوجل
|
sign_up_with_google: الاشتراك باستخدام جوجل
|
||||||
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
|
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
|
||||||
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.co/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.co/terms">شروط الخدمة</a> الخاصة بنا.'
|
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.'
|
||||||
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
|
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
|
||||||
|
|
||||||
ko:
|
ko:
|
||||||
@@ -4228,7 +4287,7 @@ ko:
|
|||||||
profile_details: 프로필 세부 정보
|
profile_details: 프로필 세부 정보
|
||||||
sign_up_with_google: Google로 가입
|
sign_up_with_google: Google로 가입
|
||||||
sign_up_with_microsoft: Microsoft로 가입
|
sign_up_with_microsoft: Microsoft로 가입
|
||||||
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.co/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.co/terms">서비스 약관</a>에 동의하는 것입니다.'
|
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하는 것입니다.'
|
||||||
enter_email_to_continue: 계속하려면 이메일을 입력하세요
|
enter_email_to_continue: 계속하려면 이메일을 입력하세요
|
||||||
|
|
||||||
en-US:
|
en-US:
|
||||||
|
|||||||
+8
-4
@@ -69,7 +69,9 @@ Rails.application.routes.draw do
|
|||||||
resource :user_initials, only: %i[edit update destroy]
|
resource :user_initials, only: %i[edit update destroy]
|
||||||
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
||||||
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
||||||
resources :submissions, only: %i[show destroy]
|
resources :submissions, only: %i[show destroy] do
|
||||||
|
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
|
||||||
|
end
|
||||||
resources :submitters, only: %i[edit update]
|
resources :submitters, only: %i[edit update]
|
||||||
resources :console_redirect, only: %i[index]
|
resources :console_redirect, only: %i[index]
|
||||||
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||||
@@ -78,8 +80,8 @@ Rails.application.routes.draw do
|
|||||||
resources :testing_api_settings, only: %i[index]
|
resources :testing_api_settings, only: %i[index]
|
||||||
resources :submitters_autocomplete, only: %i[index]
|
resources :submitters_autocomplete, only: %i[index]
|
||||||
resources :template_folders_autocomplete, only: %i[index]
|
resources :template_folders_autocomplete, only: %i[index]
|
||||||
resources :webhook_preferences, only: %i[create]
|
resources :webhook_secret, only: %i[show update]
|
||||||
resources :webhook_secret, only: %i[index create]
|
resources :webhook_preferences, only: %i[update]
|
||||||
resource :templates_upload, only: %i[create]
|
resource :templates_upload, only: %i[create]
|
||||||
authenticated do
|
authenticated do
|
||||||
resource :templates_upload, only: %i[show], path: 'new'
|
resource :templates_upload, only: %i[show], path: 'new'
|
||||||
@@ -166,7 +168,9 @@ Rails.application.routes.draw do
|
|||||||
defaults: { status: :integration }
|
defaults: { status: :integration }
|
||||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||||
resources :api, only: %i[index create], controller: 'api_settings'
|
resources :api, only: %i[index create], controller: 'api_settings'
|
||||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||||
|
post :resend
|
||||||
|
end
|
||||||
resource :account, only: %i[show update destroy]
|
resource :account, only: %i[show update destroy]
|
||||||
resources :profile, only: %i[index] do
|
resources :profile, only: %i[index] do
|
||||||
collection do
|
collection do
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2]
|
||||||
|
class MigrationWebhookUrl < ApplicationRecord
|
||||||
|
self.table_name = 'webhook_urls'
|
||||||
|
|
||||||
|
serialize :secret, coder: JSON
|
||||||
|
|
||||||
|
encrypts :url, :secret
|
||||||
|
end
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :webhook_urls, :secret, :text
|
||||||
|
|
||||||
|
MigrationWebhookUrl.all.each do |url|
|
||||||
|
url.update_columns(secret: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :webhook_urls, :secret, false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
|
||||||
|
disable_ddl_transaction
|
||||||
|
|
||||||
|
class MigrationWebhookUrl < ActiveRecord::Base
|
||||||
|
self.table_name = 'webhook_urls'
|
||||||
|
|
||||||
|
serialize :events, coder: JSON
|
||||||
|
serialize :secret, coder: JSON
|
||||||
|
|
||||||
|
encrypts :url, :secret
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationEncryptedConfig < ActiveRecord::Base
|
||||||
|
self.table_name = 'encrypted_configs'
|
||||||
|
|
||||||
|
encrypts :value
|
||||||
|
serialize :value, coder: JSON
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationAccountConfig < ActiveRecord::Base
|
||||||
|
self.table_name = 'account_configs'
|
||||||
|
|
||||||
|
serialize :value, coder: JSON
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
MigrationEncryptedConfig.where(key: 'webhook_url').find_each do |config|
|
||||||
|
webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id,
|
||||||
|
sha1: Digest::SHA1.hexdigest(config.value))
|
||||||
|
|
||||||
|
webhook_url.secret =
|
||||||
|
MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value.to_h
|
||||||
|
|
||||||
|
preferences =
|
||||||
|
MigrationAccountConfig.find_by(account_id: config.account_id, key: 'webhook_preferences')&.value.to_h
|
||||||
|
|
||||||
|
events = %w[form.viewed form.started form.completed form.declined].reject { |event| preferences[event] == false }
|
||||||
|
|
||||||
|
events += preferences.compact_blank.keys
|
||||||
|
|
||||||
|
webhook_url.events = events.uniq
|
||||||
|
webhook_url.url = config.value
|
||||||
|
|
||||||
|
webhook_url.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# 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.2].define(version: 2024_10_26_161207) do
|
ActiveRecord::Schema[7.2].define(version: 2024_10_29_192232) 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"
|
||||||
|
|
||||||
@@ -367,6 +367,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_26_161207) do
|
|||||||
t.string "sha1", null: false
|
t.string "sha1", null: false
|
||||||
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 "secret", null: false
|
||||||
t.index ["account_id"], name: "index_webhook_urls_on_account_id"
|
t.index ["account_id"], name: "index_webhook_urls_on_account_id"
|
||||||
t.index ["sha1"], name: "index_webhook_urls_on_sha1"
|
t.index ["sha1"], name: "index_webhook_urls_on_sha1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ class Ability
|
|||||||
can :manage, UserConfig, user_id: user.id
|
can :manage, UserConfig, user_id: user.id
|
||||||
can :manage, Account, id: user.account_id
|
can :manage, Account, id: user.account_id
|
||||||
can :manage, AccessToken, user_id: user.id
|
can :manage, AccessToken, user_id: user.id
|
||||||
|
can :manage, WebhookUrl, account_id: user.account_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -78,30 +78,6 @@ module Accounts
|
|||||||
new_template
|
new_template
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_webhook_url(account)
|
|
||||||
load_webhook_config(account)&.value.presence
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_webhook_config(account)
|
|
||||||
configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
|
|
||||||
if !configs && !Docuseal.multitenant? && !account.testing?
|
|
||||||
configs = Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
end
|
|
||||||
|
|
||||||
configs
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_webhook_preferences(account)
|
|
||||||
configs = account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
|
||||||
|
|
||||||
unless Docuseal.multitenant?
|
|
||||||
configs ||= Account.order(:id).first.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
|
||||||
end
|
|
||||||
|
|
||||||
configs&.value.presence || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_signing_pkcs(account)
|
def load_signing_pkcs(account)
|
||||||
cert_data =
|
cert_data =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Docuseal
|
module Docuseal
|
||||||
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
||||||
PRODUCT_URL = 'https://www.docuseal.co'
|
PRODUCT_URL = 'https://www.docuseal.com'
|
||||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||||
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
||||||
PRODUCT_NAME = 'DocuSeal'
|
PRODUCT_NAME = 'DocuSeal'
|
||||||
|
|||||||
@@ -291,7 +291,16 @@ module Submissions
|
|||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials stamp])
|
if field['type'].in?(%w[image signature initials stamp])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
|
||||||
|
image =
|
||||||
|
begin
|
||||||
|
Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
|
rescue Vips::Error
|
||||||
|
next unless attachment.content_type.starts_with?('image/')
|
||||||
|
next if attachment.byte_size.zero?
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
scale = [600.0 / image.width, 600.0 / image.height].min
|
scale = [600.0 / image.width, 600.0 / image.height].min
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ module Submissions
|
|||||||
name: column_name(I18n.t('phone'), submitter_name, submitters_count),
|
name: column_name(I18n.t('phone'), submitter_name, submitters_count),
|
||||||
value: submitter.phone
|
value: submitter.phone
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: column_name(I18n.t('status'), submitter_name, submitters_count),
|
||||||
|
value: submitter.status
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
|
name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
|
||||||
value: submitter.completed_at.to_s
|
value: submitter.completed_at.to_s
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module Submissions
|
|||||||
'Helvetica'
|
'Helvetica'
|
||||||
end
|
end
|
||||||
|
|
||||||
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
|
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
|
||||||
|
|
||||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ module Submissions
|
|||||||
MAX_PAGE_ROTATE = 20
|
MAX_PAGE_ROTATE = 20
|
||||||
|
|
||||||
A4_SIZE = [595, 842].freeze
|
A4_SIZE = [595, 842].freeze
|
||||||
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
|
||||||
|
|
||||||
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ module Submissions
|
|||||||
name: item['name'])
|
name: item['name'])
|
||||||
end
|
end
|
||||||
|
|
||||||
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
|
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
|
||||||
|
|
||||||
images_pdf =
|
images_pdf =
|
||||||
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
||||||
@@ -88,7 +87,9 @@ module Submissions
|
|||||||
name: template.name
|
name: template.name
|
||||||
)
|
)
|
||||||
|
|
||||||
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
ApplicationRecord.no_touching do
|
||||||
|
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdfs(submitter)
|
def generate_pdfs(submitter)
|
||||||
@@ -178,6 +179,8 @@ module Submissions
|
|||||||
font_size = preferences_font_size
|
font_size = preferences_font_size
|
||||||
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
|
|
||||||
|
fill_color = field.dig('preferences', 'color').presence
|
||||||
|
|
||||||
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
|
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
@@ -273,7 +276,15 @@ module Submissions
|
|||||||
|
|
||||||
attachments_data_cache[attachment.uuid] ||= attachment.download
|
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||||
|
|
||||||
image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
image =
|
||||||
|
begin
|
||||||
|
Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
||||||
|
rescue Vips::Error
|
||||||
|
next unless attachment.content_type.starts_with?('image/')
|
||||||
|
next if attachment.byte_size.zero?
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
scale = [(area['w'] * width) / image.width,
|
scale = [(area['w'] * width) / image.width,
|
||||||
(area['h'] * height) / image.height].min
|
(area['h'] * height) / image.height].min
|
||||||
@@ -371,6 +382,7 @@ module Submissions
|
|||||||
next if char.blank?
|
next if char.blank?
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(char, font:,
|
text = HexaPDF::Layout::TextFragment.create(char, font:,
|
||||||
|
fill_color:,
|
||||||
font_size:)
|
font_size:)
|
||||||
|
|
||||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||||
@@ -378,6 +390,7 @@ module Submissions
|
|||||||
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
||||||
text = HexaPDF::Layout::TextFragment.create(char,
|
text = HexaPDF::Layout::TextFragment.create(char,
|
||||||
font:,
|
font:,
|
||||||
|
fill_color:,
|
||||||
font_size: (font_size / 1.4).to_i)
|
font_size: (font_size / 1.4).to_i)
|
||||||
|
|
||||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||||
@@ -386,6 +399,7 @@ module Submissions
|
|||||||
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
||||||
text = HexaPDF::Layout::TextFragment.create(char,
|
text = HexaPDF::Layout::TextFragment.create(char,
|
||||||
font:,
|
font:,
|
||||||
|
fill_color:,
|
||||||
font_size: (font_size / 1.9).to_i)
|
font_size: (font_size / 1.9).to_i)
|
||||||
|
|
||||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||||
@@ -405,6 +419,7 @@ module Submissions
|
|||||||
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(value, font:,
|
text = HexaPDF::Layout::TextFragment.create(value, font:,
|
||||||
|
fill_color:,
|
||||||
font_size:)
|
font_size:)
|
||||||
|
|
||||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||||
@@ -413,6 +428,7 @@ module Submissions
|
|||||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||||
text = HexaPDF::Layout::TextFragment.create(value,
|
text = HexaPDF::Layout::TextFragment.create(value,
|
||||||
font:,
|
font:,
|
||||||
|
fill_color:,
|
||||||
font_size: (font_size / 1.4).to_i)
|
font_size: (font_size / 1.4).to_i)
|
||||||
|
|
||||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||||
@@ -423,6 +439,7 @@ module Submissions
|
|||||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||||
text = HexaPDF::Layout::TextFragment.create(value,
|
text = HexaPDF::Layout::TextFragment.create(value,
|
||||||
font:,
|
font:,
|
||||||
|
fill_color:,
|
||||||
font_size: (font_size / 1.9).to_i)
|
font_size: (font_size / 1.9).to_i)
|
||||||
|
|
||||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module Submissions
|
|||||||
|
|
||||||
TimestampError = Class.new(StandardError)
|
TimestampError = Class.new(StandardError)
|
||||||
|
|
||||||
attr_reader :tsa_url
|
attr_reader :tsa_url, :tsa_fallback_url
|
||||||
|
|
||||||
def initialize(tsa_url:)
|
def initialize(tsa_url:)
|
||||||
@tsa_url = tsa_url
|
@tsa_url, @tsa_fallback_url = tsa_url.split(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
def finalize_objects(_signature_field, signature)
|
def finalize_objects(_signature_field, signature)
|
||||||
@@ -37,7 +37,16 @@ module Submissions
|
|||||||
response = conn.post(uri.path, build_payload(digest.digest),
|
response = conn.post(uri.path, build_payload(digest.digest),
|
||||||
'content-type' => 'application/timestamp-query')
|
'content-type' => 'application/timestamp-query')
|
||||||
|
|
||||||
raise TimestampError if response.status != 200 || response.body.blank?
|
if response.status != 200 || response.body.blank?
|
||||||
|
raise TimestampError if tsa_fallback_url.blank?
|
||||||
|
|
||||||
|
Rollbar.error('TimestampError: use fallback URL') if defined?(Rollbar)
|
||||||
|
|
||||||
|
response = Faraday.post(tsa_fallback_url, build_payload(digest.digest),
|
||||||
|
'content-type' => 'application/timestamp-query')
|
||||||
|
|
||||||
|
raise TimestampError if response.status != 200 || response.body.blank?
|
||||||
|
end
|
||||||
|
|
||||||
OpenSSL::Timestamp::Response.new(response.body).token.to_der
|
OpenSSL::Timestamp::Response.new(response.body).token.to_der
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,14 +14,17 @@ module Submitters
|
|||||||
unless submitter.submission_events.exists?(event_type: 'start_form')
|
unless submitter.submission_events.exists?(event_type: 'start_form')
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
|
||||||
|
|
||||||
SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
WebhookUrls.for_account_id(submitter.account_id, 'form.started').each do |webhook_url|
|
||||||
|
SendFormStartedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
update_submitter!(submitter, params, request)
|
update_submitter!(submitter, params, request)
|
||||||
|
|
||||||
submitter.submission.save!
|
submitter.submission.save!
|
||||||
|
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ module Templates
|
|||||||
|
|
||||||
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
|
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
|
||||||
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
|
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
|
||||||
|
SELECT_PLACEHOLDER_REGEXP = /\b(
|
||||||
|
Select |
|
||||||
|
Choose |
|
||||||
|
Wählen |
|
||||||
|
Auswählen |
|
||||||
|
Sélectionner|
|
||||||
|
Choisir |
|
||||||
|
Seleccionar |
|
||||||
|
Elegir |
|
||||||
|
Seleziona |
|
||||||
|
Scegliere |
|
||||||
|
Selecionar |
|
||||||
|
Escolher
|
||||||
|
)\b/ix
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -143,8 +157,8 @@ module Templates
|
|||||||
{
|
{
|
||||||
**attrs,
|
**attrs,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: build_options(field[:Opt]),
|
options: build_options(field[:Opt], 'select'),
|
||||||
default_value: field.field_value
|
default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value
|
||||||
}
|
}
|
||||||
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
|
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
|
||||||
{
|
{
|
||||||
@@ -178,11 +192,14 @@ module Templates
|
|||||||
def build_options(values, type = nil)
|
def build_options(values, type = nil)
|
||||||
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
|
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
|
||||||
|
|
||||||
values.map do |option|
|
values.filter_map do |option|
|
||||||
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
|
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
|
||||||
|
|
||||||
|
option = option[1] if option.is_a?(Array) && option.size == 2
|
||||||
option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
|
option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
|
||||||
|
|
||||||
|
next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
|
||||||
|
|
||||||
{
|
{
|
||||||
uuid: SecureRandom.uuid,
|
uuid: SecureRandom.uuid,
|
||||||
value: is_option_number || is_skip_single_value ? '' : option
|
value: is_option_number || is_skip_single_value ? '' : option
|
||||||
|
|||||||
@@ -122,6 +122,15 @@ module Templates
|
|||||||
|
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
|
pdf.acro_form.each_field do |field|
|
||||||
|
next if field.field_type != :Ch ||
|
||||||
|
field[:Opt].blank? ||
|
||||||
|
%i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
|
||||||
|
!field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
|
||||||
|
|
||||||
|
field[:V] = ''
|
||||||
|
end
|
||||||
|
|
||||||
pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
|
pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
|
||||||
pdf.acro_form.flatten
|
pdf.acro_form.flatten
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,15 @@ module Templates
|
|||||||
name: :preview_images)
|
name: :preview_images)
|
||||||
.preload(:blob)
|
.preload(:blob)
|
||||||
|
|
||||||
json[:documents] = template.schema.map do |item|
|
json[:documents] = template.schema.filter_map do |item|
|
||||||
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
|
unless attachment
|
||||||
|
Rollbar.error("Documents missing: #{template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
|
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
|
||||||
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WebhookUrls
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def for_account_id(account_id, events)
|
||||||
|
events = Array.wrap(events)
|
||||||
|
|
||||||
|
rel = WebhookUrl.where(account_id:)
|
||||||
|
|
||||||
|
event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or)
|
||||||
|
|
||||||
|
if Docuseal.multitenant? || account_id == 1
|
||||||
|
rel.where(event_arel)
|
||||||
|
else
|
||||||
|
linked_account_rel =
|
||||||
|
AccountLinkedAccount.where(linked_account_id: account_id).where.not(account_type: :testing).select(:account_id)
|
||||||
|
|
||||||
|
webhook_urls = rel.or(WebhookUrl.where(account_id: linked_account_rel).where(event_arel))
|
||||||
|
|
||||||
|
account_urls, linked_urls = webhook_urls.partition { |w| w.account_id == account_id }
|
||||||
|
|
||||||
|
account_urls.select { |w| w.events.intersect?(events) }.presence ||
|
||||||
|
(account_urls.present? ? WebhookUrl.none : linked_urls)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :webhook_url do
|
||||||
|
account
|
||||||
|
url { Faker::Internet.url }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendFormCompletedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitter) do
|
||||||
|
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||||
|
end
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['form.completed']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.completed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.completed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['form.declined'])
|
||||||
|
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submitter_id']).to eq(submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendFormDeclinedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitter) do
|
||||||
|
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||||
|
end
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['form.declined']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.declined',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.declined',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['form.completed'])
|
||||||
|
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submitter_id']).to eq(submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendFormStartedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitter) do
|
||||||
|
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||||
|
end
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['form.started']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.started',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.started',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['form.declined'])
|
||||||
|
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submitter_id']).to eq(submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendFormViewedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitter) do
|
||||||
|
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||||
|
end
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['form.viewed']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.viewed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'form.viewed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['form.started'])
|
||||||
|
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submitter_id']).to eq(submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendSubmissionArchivedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['submission.archived']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.archived',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.archived',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['submission.created'])
|
||||||
|
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submission_id']).to eq(submission.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||||
|
'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendSubmissionCompletedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) }
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['submission.completed']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.completed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.completed',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['submission.archived'])
|
||||||
|
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submission_id']).to eq(submission.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||||
|
'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendSubmissionCreatedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) }
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['submission.created']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.created',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'submission.created',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['submission.completed'])
|
||||||
|
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submission_id']).to eq(submission.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
|
||||||
|
'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendTemplateCreatedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['template.created']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'template.created',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'template.created',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['template.updated'])
|
||||||
|
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['template_id']).to eq(template.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SendTemplateUpdatedWebhookRequestJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:webhook_url) { create(:webhook_url, account:, events: ['template.updated']) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request' do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'template.updated',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a webhook request with the secret' do
|
||||||
|
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).with(
|
||||||
|
body: {
|
||||||
|
'event_type' => 'template.updated',
|
||||||
|
'timestamp' => /.*/,
|
||||||
|
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => 'DocuSeal.com Webhook',
|
||||||
|
'X-Secret-Header' => 'secret_value'
|
||||||
|
}
|
||||||
|
).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send a webhook request if the event is not in the webhook's events" do
|
||||||
|
webhook_url.update!(events: ['template.created'])
|
||||||
|
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
|
||||||
|
expect(WebMock).not_to have_requested(:post, webhook_url.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends again if the response status is 400 or higher' do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
|
||||||
|
end.to change(described_class.jobs, :size).by(1)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
|
||||||
|
args = described_class.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['attempt']).to eq(1)
|
||||||
|
expect(args['last_status']).to eq(401)
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['template_id']).to eq(template.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't send again if the max attempts is reached" do
|
||||||
|
stub_request(:post, webhook_url.url).to_return(status: 401)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
|
||||||
|
end.not_to change(described_class.jobs, :size)
|
||||||
|
|
||||||
|
expect(WebMock).to have_requested(:post, webhook_url.url).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -44,7 +44,9 @@ RSpec.describe 'Template' do
|
|||||||
|
|
||||||
it 'archives a template' do
|
it 'archives a template' do
|
||||||
expect do
|
expect do
|
||||||
click_button 'Archive'
|
accept_confirm('Are you sure?') do
|
||||||
|
click_button 'Archive'
|
||||||
|
end
|
||||||
end.to change { Template.active.count }.by(-1)
|
end.to change { Template.active.count }.by(-1)
|
||||||
|
|
||||||
expect(page).to have_content('Template has been archived')
|
expect(page).to have_content('Template has been archived')
|
||||||
|
|||||||
@@ -8,24 +8,182 @@ RSpec.describe 'Webhook Settings' do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows webhook settings page with empty form when there are no webhooks' do
|
||||||
visit settings_webhooks_path
|
visit settings_webhooks_path
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows webhook settings page' do
|
expect(page).to have_content('Webhook')
|
||||||
expect(page).to have_content('Webhooks')
|
expect(page).to have_content('Webhook URL')
|
||||||
expect(page).to have_field('Webhook URL')
|
expect(page).to have_field('webhook_url[url]', type: 'url')
|
||||||
expect(page).to have_button('Save')
|
expect(page).to have_button('Save')
|
||||||
|
|
||||||
|
WebhookUrl::EVENTS.each do |event|
|
||||||
|
expect(page).to have_field(event, type: 'checkbox')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the webhook URL' do
|
it 'shows list of webhooks when there are more than one' do
|
||||||
fill_in 'Webhook URL', with: 'https://example.com'
|
webhook_urls = create_list(:webhook_url, 2, account:)
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect(page).to have_content('Webhooks')
|
||||||
|
expect(page).to have_link('New Webhook')
|
||||||
|
|
||||||
|
webhook_urls.each do |webhook_url|
|
||||||
|
expect(page).to have_content(webhook_url.url)
|
||||||
|
|
||||||
|
within("a[href='#{settings_webhook_path(webhook_url)}']") do
|
||||||
|
webhook_url.events.each do |event|
|
||||||
|
expect(page).to have_content(event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows webhook settings page with pre-filled form when there is one webhook' do
|
||||||
|
webhook_url = create(:webhook_url, account:)
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect(page).to have_content('Webhook')
|
||||||
|
expect(page).to have_field('webhook_url[url]', type: 'url', with: webhook_url.url)
|
||||||
|
expect(page).to have_button('Save')
|
||||||
|
expect(page).to have_button('Delete')
|
||||||
|
expect(page).to have_link('Add Secret')
|
||||||
|
|
||||||
|
WebhookUrl::EVENTS.each do |event|
|
||||||
|
expect(page).to have_field(event, type: 'checkbox', checked: webhook_url.events.include?(event))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the webhook' do
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
fill_in 'webhook_url[url]', with: 'https://example.com/webhook'
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
click_button 'Save'
|
click_button 'Save'
|
||||||
end.to change(EncryptedConfig, :count).by(1)
|
end.to change(WebhookUrl, :count).by(1)
|
||||||
|
|
||||||
encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::WEBHOOK_URL_KEY)
|
webhook_url = account.webhook_urls.first
|
||||||
|
|
||||||
expect(encrypted_config.value).to eq('https://example.com')
|
expect(webhook_url.url).to eq('https://example.com/webhook')
|
||||||
|
expect(page).to have_content('Webhook URL has been saved.')
|
||||||
|
expect(page.current_path).to eq(settings_webhooks_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the webhook' do
|
||||||
|
webhook_url = create(:webhook_url, account:, url: 'https://example.com/webhook')
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
fill_in 'webhook_url[url]', with: 'https://example.org/webhook'
|
||||||
|
click_button 'Save'
|
||||||
|
|
||||||
|
webhook_url.reload
|
||||||
|
|
||||||
|
expect(webhook_url.url).to eq('https://example.org/webhook')
|
||||||
|
expect(page).to have_content('Webhook URL has been updated.')
|
||||||
|
expect(page.current_path).to eq(settings_webhooks_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the webhook' do
|
||||||
|
create(:webhook_url, account:)
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect do
|
||||||
|
accept_confirm('Are you sure?') do
|
||||||
|
click_button 'Delete'
|
||||||
|
end
|
||||||
|
end.to change(WebhookUrl, :count).by(-1)
|
||||||
|
|
||||||
|
expect(page).to have_content('Webhook URL has been deleted.')
|
||||||
|
expect(page.current_path).to eq(settings_webhooks_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the webhook events' do
|
||||||
|
webhook_url = create(:webhook_url, account:)
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect(webhook_url.events).not_to include('submission.created')
|
||||||
|
|
||||||
|
check('submission.created')
|
||||||
|
|
||||||
|
webhook_url.reload
|
||||||
|
|
||||||
|
expect(webhook_url.events).to include('submission.created')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds a secret to the webhook' do
|
||||||
|
webhook_url = create(:webhook_url, account:)
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect(webhook_url.secret).to eq({})
|
||||||
|
|
||||||
|
click_link 'Add Secret'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'Key', with: 'X-Signature'
|
||||||
|
fill_in 'Value', with: 'secret-value'
|
||||||
|
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
webhook_url.reload
|
||||||
|
|
||||||
|
expect(webhook_url.secret).to eq({ 'X-Signature' => 'secret-value' })
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_link('Edit Secret')
|
||||||
|
expect(page).to have_content('Webhook Secret has been saved.')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes a secret from the webhook' do
|
||||||
|
webhook_url = create(:webhook_url, account:, secret: { 'X-Signature' => 'secret-value' })
|
||||||
|
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
click_link 'Edit Secret'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'Key', with: ''
|
||||||
|
fill_in 'Value', with: ''
|
||||||
|
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
webhook_url.reload
|
||||||
|
|
||||||
|
expect(webhook_url.secret).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_link('Add Secret')
|
||||||
|
expect(page).to have_content('Webhook Secret has been saved.')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when testing the webhook' do
|
||||||
|
let!(:webhook_url) { create(:webhook_url, account:) }
|
||||||
|
let!(:template) { create(:template, account:, author: user) }
|
||||||
|
let!(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let!(:submitter) do
|
||||||
|
create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends the webhook request' do
|
||||||
|
visit settings_webhooks_path
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Test Webhook'
|
||||||
|
end.to change(SendFormCompletedWebhookRequestJob.jobs, :size).by(1)
|
||||||
|
|
||||||
|
args = SendFormCompletedWebhookRequestJob.jobs.last['args'].first
|
||||||
|
|
||||||
|
expect(args['webhook_url_id']).to eq(webhook_url.id)
|
||||||
|
expect(args['submitter_id']).to eq(submitter.id)
|
||||||
|
expect(page).to have_content('Webhook request has been sent.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user