mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7a895b98b | ||
|
|
b90b7b7845 | ||
|
|
3d05d75f2d | ||
|
|
e701881377 | ||
|
|
4615e3e48f | ||
|
|
988a5361a6 | ||
|
|
1b60b42428 | ||
|
|
7b74487214 | ||
|
|
91d98ed267 | ||
|
|
24fbb7de06 | ||
|
|
91df5b2407 | ||
|
|
eef2d9d599 | ||
|
|
8ad70b7e74 | ||
|
|
8b775103c7 | ||
|
|
8c2475aac1 | ||
|
|
d3236ae4a1 | ||
|
|
8fd9af3a6a | ||
|
|
4f94858901 | ||
|
|
d0ac4ad222 | ||
|
|
10e4476f4a | ||
|
|
74411d883a | ||
|
|
18ee2d5316 | ||
|
|
b4f80422eb | ||
|
|
7fe9ab7601 | ||
|
|
13ad614e6b | ||
|
|
7b1b4bcf2c | ||
|
|
a6715d4034 | ||
|
|
254995c6c6 | ||
|
|
0e33e23d1f | ||
|
|
57503eefd2 | ||
|
|
cd477c880c | ||
|
|
bf2f5b24dc | ||
|
|
7eba4865bc | ||
|
|
64cd24a24d | ||
|
|
de5acd0949 | ||
|
|
6ebbc3edac | ||
|
|
2020f6c136 | ||
|
|
ee4acd5a82 | ||
|
|
2e487ac936 | ||
|
|
dee5fc7c07 | ||
|
|
e130584fec | ||
|
|
ade5c4832f | ||
|
|
33b14d5278 | ||
|
|
48e73c2c81 | ||
|
|
894d98e92b | ||
|
|
14c84eff93 | ||
|
|
01d982b093 | ||
|
|
35852ef6ab | ||
|
|
47b235dc5f | ||
|
|
0e11d2a581 | ||
|
|
e57e11f206 | ||
|
|
51f639065c | ||
|
|
6c1d4b466f | ||
|
|
b940c597dc | ||
|
|
1ce67bcb39 | ||
|
|
5050a67d08 | ||
|
|
76321cef3c | ||
|
|
8f3a203994 | ||
|
|
ea24562861 | ||
|
|
4a888d048e | ||
|
|
36331bb993 | ||
|
|
6f0e7c0ca8 | ||
|
|
2491b7b7e3 | ||
|
|
7965c17f46 | ||
|
|
5519459519 | ||
|
|
d8bf2cc101 | ||
|
|
2aece98d67 | ||
|
|
f602497255 | ||
|
|
11db68cdc7 | ||
|
|
bed98f4344 |
No files matched your search
@@ -6,6 +6,7 @@ jobs:
|
|||||||
rubocop:
|
rubocop:
|
||||||
name: Rubocop
|
name: Rubocop
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -30,6 +31,7 @@ jobs:
|
|||||||
erblint:
|
erblint:
|
||||||
name: Erblint
|
name: Erblint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -54,6 +56,7 @@ jobs:
|
|||||||
eslint:
|
eslint:
|
||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
@@ -80,6 +83,7 @@ jobs:
|
|||||||
brakeman:
|
brakeman:
|
||||||
name: Brakeman
|
name: Brakeman
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -107,6 +111,7 @@ jobs:
|
|||||||
rspec:
|
rspec:
|
||||||
name: RSpec
|
name: RSpec
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
|
|||||||
Max: 50
|
Max: 50
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 9
|
Max: 15
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 5
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ gem 'rails'
|
|||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
|
gem 'rouge', require: false
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.0)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
|
rouge (4.5.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
@@ -632,6 +633,7 @@ DEPENDENCIES
|
|||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
rotp
|
rotp
|
||||||
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
|
||||||
|
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
|
|
||||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||||
|
|||||||
@@ -63,12 +63,7 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
submissions.each do |submission|
|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
@@ -96,10 +91,7 @@ module Api
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||||
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])
|
||||||
@@ -112,7 +104,9 @@ module Api
|
|||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:template_folder].present?
|
if params[:template_folder].present?
|
||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
|
||||||
|
|
||||||
|
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.key?(:archived)
|
if params.key?(:archived)
|
||||||
@@ -185,11 +179,12 @@ module Api
|
|||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:expire_at, :name,
|
:require_phone_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
|
:require_phone_2fa,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ module Api
|
|||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ module Api
|
|||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
@@ -193,6 +193,10 @@ module Api
|
|||||||
|
|
||||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
if submitter_preferences.key?('require_phone_2fa')
|
||||||
|
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||||
|
end
|
||||||
|
|
||||||
if submitter_preferences.key?('go_to_last')
|
if submitter_preferences.key?('go_to_last')
|
||||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ module Api
|
|||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(cloned_template)
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,7 @@ module Api
|
|||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
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
|
||||||
@@ -93,7 +90,12 @@ module Api
|
|||||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
|
||||||
|
if params[:folder].present?
|
||||||
|
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
|
||||||
|
|
||||||
|
templates = templates.where(folder_id: folder_ids)
|
||||||
|
end
|
||||||
|
|
||||||
templates
|
templates
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
|||||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
|
'/submissions/html',
|
||||||
|
'/api/submissions/html',
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
'/api/templates/html',
|
'/api/templates/html',
|
||||||
|
'/submissions/pdf',
|
||||||
|
'/api/submissions/pdf',
|
||||||
'/templates/pdf',
|
'/templates/pdf',
|
||||||
'/api/templates/pdf',
|
'/api/templates/pdf',
|
||||||
'/templates/doc',
|
'/templates/doc',
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SearchEntriesReindexController < ApplicationController
|
||||||
|
def create
|
||||||
|
authorize!(:manage, EncryptedConfig)
|
||||||
|
|
||||||
|
ReindexAllSearchEntriesJob.perform_async
|
||||||
|
|
||||||
|
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
|
||||||
|
.update!(value: true)
|
||||||
|
|
||||||
|
Docuseal.instance_variable_set(:@fulltext_search, nil)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_account_path,
|
||||||
|
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,11 +11,16 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if params[:template_slug]
|
if params[:template_slug]
|
||||||
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
template = Template.find_by!(slug: params[:template_slug])
|
||||||
template: { slug: params[:template_slug] })
|
|
||||||
|
@submitter =
|
||||||
|
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||||
elsif params[:submission_slug]
|
elsif params[:submission_slug]
|
||||||
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
submission = Submission.find_by(slug: params[:submission_slug])
|
||||||
submission: { slug: params[:submission_slug] })
|
|
||||||
|
if submission
|
||||||
|
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||||
|
end
|
||||||
|
|
||||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||||
else
|
else
|
||||||
@@ -24,14 +29,18 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
|
||||||
end
|
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
f.json { head :ok }
|
f.json { head :ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def already_sent?(submitter)
|
||||||
|
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
|||||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||||
]
|
]
|
||||||
@account.encrypted_configs.create!(encrypted_configs)
|
@account.encrypted_configs.create!(encrypted_configs)
|
||||||
|
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||||
|
|
||||||
Docuseal.refresh_default_url_options!
|
Docuseal.refresh_default_url_options!
|
||||||
|
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ class StartFormController < ApplicationController
|
|||||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
||||||
else
|
else
|
||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||||
@error_message = multiple_submitters_error_message
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show
|
return render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if (is_new_record = @submitter.new_record?)
|
if (is_new_record = @submitter.new_record?)
|
||||||
@@ -49,9 +49,9 @@ class StartFormController < ApplicationController
|
|||||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.errors.blank? && @submitter.save
|
||||||
if is_new_record
|
if is_new_record
|
||||||
enqueue_submission_create_webhooks(@submitter)
|
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@submitter)
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -71,9 +71,20 @@ class StartFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
||||||
|
|
||||||
|
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
|
||||||
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
|
||||||
|
|
||||||
|
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
|
||||||
|
required_params.except('name').compact_blank.blank?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(email: params[:email])
|
.find_by!(required_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -96,14 +107,16 @@ class StartFormController < ApplicationController
|
|||||||
redirect_to start_form_path(@template.slug)
|
redirect_to start_form_path(@template.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_submission_create_webhooks(submitter)
|
|
||||||
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
|
|
||||||
|
|
||||||
def find_or_initialize_submitter(template, submitter_params)
|
def find_or_initialize_submitter(template, submitter_params)
|
||||||
|
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||||
|
|
||||||
|
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||||
|
|
||||||
|
find_params = required_params.except('name')
|
||||||
|
|
||||||
|
submitter = Submitter.new if find_params.compact_blank.blank?
|
||||||
|
|
||||||
|
submitter ||=
|
||||||
Submitter
|
Submitter
|
||||||
.where(submission: template.submissions.where(expire_at: Time.current..)
|
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||||
@@ -112,7 +125,17 @@ class StartFormController < ApplicationController
|
|||||||
.where(external_id: nil)
|
.where(external_id: nil)
|
||||||
.where(ip: [nil, request.remote_ip])
|
.where(ip: [nil, request.remote_ip])
|
||||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
.find_or_initialize_by(find_params)
|
||||||
|
|
||||||
|
submitter.name = required_params['name'] if submitter.new_record?
|
||||||
|
|
||||||
|
unless @resubmit_submitter
|
||||||
|
required_params.each do |key, value|
|
||||||
|
submitter.errors.add(key.to_sym, :blank) if value.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_submission_attributes(submitter, template)
|
def assign_submission_attributes(submitter, template)
|
||||||
@@ -125,6 +148,8 @@ class StartFormController < ApplicationController
|
|||||||
metadata: @resubmit_submitter&.metadata.presence || {}
|
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
|
||||||
|
|
||||||
if submitter.values.present?
|
if submitter.values.present?
|
||||||
@resubmit_submitter.attachments.each do |attachment|
|
@resubmit_submitter.attachments.each do |attachment|
|
||||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class SubmissionsController < ApplicationController
|
|||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
enqueue_submission_created_webhooks(@template, submissions)
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
@@ -77,10 +77,7 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||||
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
|
||||||
@@ -97,15 +94,6 @@ 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
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
raise ActionController::RoutingError, I18n.t('not_found')
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
end
|
end
|
||||||
|
|
||||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
if use_signature?(@submission) && !signature_valid
|
||||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
@@ -48,9 +48,15 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def submission_valid_ttl?(submission)
|
def use_signature?(submission)
|
||||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
return false if current_user && can?(:read, submission)
|
||||||
|
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
||||||
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
|
||||||
|
!submission_valid_ttl?(submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
last_submitter && last_submitter.completed_at > TTL.ago
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||||
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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.completed_at? &&
|
!submitter.completed_at? &&
|
||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template.archived_at?
|
!submitter.submission.template&.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||||
return render json: {} if submitter.submission.template.archived_at? ||
|
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired?
|
submitter.submission.expired?
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
|
|
||||||
def search_submitters(submitters)
|
def search_submitters(submitters)
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
if Docuseal.fulltext_search?(current_user)
|
if Docuseal.fulltext_search?
|
||||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||||
else
|
else
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ class TemplateFoldersAutocompleteController < ApplicationController
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
||||||
|
|
||||||
|
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
class TemplateFoldersController < ApplicationController
|
class TemplateFoldersController < ApplicationController
|
||||||
load_and_authorize_resource :template_folder
|
load_and_authorize_resource :template_folder
|
||||||
|
|
||||||
|
helper_method :selected_order
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||||
.preload(:author, :template_accesses)
|
.preload(:author, :template_accesses)
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||||
end
|
end
|
||||||
@@ -25,6 +27,16 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def selected_order
|
||||||
|
@selected_order ||=
|
||||||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||||
|
'created_at'
|
||||||
|
else
|
||||||
|
cookies.permanent[:dashboard_templates_order]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_folder_params
|
def template_folder_params
|
||||||
params.require(:template_folder).permit(:name)
|
params.require(:template_folder).permit(:name)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
enqueue_template_created_webhooks(@template)
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
@@ -91,7 +91,7 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
||||||
|
|
||||||
enqueue_template_updated_webhooks(@template)
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -142,20 +142,6 @@ 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?
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
TEMPLATES_PER_PAGE = 12
|
TEMPLATES_PER_PAGE = 12
|
||||||
FOLDERS_PER_PAGE = 18
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
helper_method :selected_order
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
@template_folders = sort_template_folders(@template_folders, current_user,
|
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
|
||||||
cookies.permanent[:dashboard_templates_order])
|
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -26,7 +27,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
||||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
limit =
|
limit =
|
||||||
if @template_folders.size < 4
|
if @template_folders.size < 4
|
||||||
@@ -36,6 +37,8 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -53,10 +56,8 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
|
|
||||||
rel = Template.where(
|
rel = Template.where(
|
||||||
Template.arel_table[:id].in(
|
Template.arel_table[:id].in(
|
||||||
Arel::Nodes::Union.new(
|
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
||||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel,
|
.union(:all, shared_template_ids.arel)
|
||||||
shared_template_ids.arel
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -100,4 +101,29 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
template_folders.order(id: :desc)
|
template_folders.order(id: :desc)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def selected_order
|
||||||
|
@selected_order ||=
|
||||||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||||
|
'created_at'
|
||||||
|
else
|
||||||
|
cookies.permanent[:dashboard_templates_order]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions = Submission.accessible_by(current_ability)
|
||||||
|
.left_joins(:template)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -25,13 +25,13 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
default_expire_at_duration
|
default_expire_at_duration
|
||||||
default_expire_at
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ class TemplatesShareLinkController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
if params[:redir].present?
|
||||||
|
redirect_to params[:redir]
|
||||||
|
else
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
enqueue_template_created_webhooks(@template)
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
@@ -68,11 +68,4 @@ 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
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookEventsController < ApplicationController
|
||||||
|
load_and_authorize_resource :webhook_url, parent: false, id_param: :webhook_id
|
||||||
|
before_action :load_webhook_event
|
||||||
|
|
||||||
|
def show
|
||||||
|
return unless current_ability.can?(:read, @webhook_event.record)
|
||||||
|
|
||||||
|
@data =
|
||||||
|
case @webhook_event.event_type
|
||||||
|
when 'form.started', 'form.completed', 'form.declined', 'form.viewed'
|
||||||
|
Submitters::SerializeForWebhook.call(@webhook_event.record)
|
||||||
|
when 'submission.created', 'submission.completed', 'submission.expired'
|
||||||
|
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||||
|
when 'template.created', 'template.updated'
|
||||||
|
Templates::SerializeForApi.call(@webhook_event.record)
|
||||||
|
when 'submission.archived'
|
||||||
|
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend
|
||||||
|
id_key = WebhookUrls::EVENT_TYPE_ID_KEYS.fetch(@webhook_event.event_type.split('.').first)
|
||||||
|
|
||||||
|
last_attempt_id = @webhook_event.webhook_attempts.maximum(:id)
|
||||||
|
|
||||||
|
WebhookUrls::EVENT_TYPE_TO_JOB_CLASS[@webhook_event.event_type].perform_async(
|
||||||
|
id_key => @webhook_event.record_id,
|
||||||
|
'webhook_url_id' => @webhook_event.webhook_url_id,
|
||||||
|
'event_uuid' => @webhook_event.uuid,
|
||||||
|
'attempt' => SendWebhookRequest::MANUAL_ATTEMPT,
|
||||||
|
'last_status' => 0
|
||||||
|
)
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.after(
|
||||||
|
params[:button_id],
|
||||||
|
helpers.tag.submit_form(
|
||||||
|
helpers.button_to('', refresh_settings_webhook_event_path(@webhook_url.id, @webhook_event.uuid),
|
||||||
|
params: { last_attempt_id: }),
|
||||||
|
class: 'hidden', data: { interval: 3_000 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh
|
||||||
|
return head :ok if @webhook_event.webhook_attempts.maximum(:id) == params[:last_attempt_id].to_i
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.replace(helpers.dom_id(@webhook_event),
|
||||||
|
partial: 'event_row',
|
||||||
|
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
|
||||||
|
turbo_stream.replace("drawer_events_#{helpers.dom_id(@webhook_event)}",
|
||||||
|
partial: 'drawer_events',
|
||||||
|
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_webhook_event
|
||||||
|
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,10 +8,26 @@ class WebhookSettingsController < ApplicationController
|
|||||||
@webhook_urls = @webhook_urls.order(id: :desc)
|
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||||
@webhook_url = @webhook_urls.first_or_initialize
|
@webhook_url = @webhook_urls.first_or_initialize
|
||||||
|
|
||||||
render @webhook_urls.size > 1 ? 'index' : 'show'
|
if @webhook_urls.size > 1
|
||||||
|
render :index
|
||||||
|
else
|
||||||
|
@webhook_events = @webhook_url.webhook_events
|
||||||
|
|
||||||
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
|
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@webhook_events = @webhook_url.webhook_events
|
||||||
|
|
||||||
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
|
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||||
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
@@ -37,13 +53,18 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def resend
|
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
|
||||||
|
|
||||||
|
authorize!(:read, submitter)
|
||||||
|
|
||||||
if submitter.blank? || @webhook_url.blank?
|
if submitter.blank? || @webhook_url.blank?
|
||||||
return redirect_back(fallback_location: settings_webhooks_path,
|
return redirect_back(fallback_location: settings_webhooks_path,
|
||||||
alert: I18n.t('unable_to_resend_webhook_request'))
|
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||||
end
|
end
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
SendTestWebhookRequestJob.perform_async(
|
||||||
'webhook_url_id' => @webhook_url.id)
|
'submitter_id' => submitter.id,
|
||||||
|
'event_uuid' => SecureRandom.uuid,
|
||||||
|
'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
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import SetDateButton from './elements/set_date_button'
|
|||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
|||||||
safeRegisterElement('app-tour', AppTour)
|
safeRegisterElement('app-tour', AppTour)
|
||||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
safeRegisterElement('check-on-click', CheckOnClick)
|
safeRegisterElement('check-on-click', CheckOnClick)
|
||||||
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('click', () => {
|
this.addEventListener('click', () => {
|
||||||
if (!this.element.checked) {
|
if (this.element && !this.element.disabled && !this.element.checked) {
|
||||||
this.element.checked = true
|
this.element.checked = true
|
||||||
this.element.dispatchEvent(new Event('change', { bubbles: true }))
|
this.element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', this.handleChange)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = () => {
|
||||||
|
if (this.checkedCount !== 0) {
|
||||||
|
this.closest('form')?.requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get checkedCount () {
|
||||||
|
return this.querySelectorAll('input[type="checkbox"]:checked').length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
if (this.dataset.interval) {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}, parseInt(this.dataset.interval))
|
||||||
|
} else {
|
||||||
this.querySelector('form').requestSubmit()
|
this.querySelector('form').requestSubmit()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
must_be_characters_length: 'Must be {number} characters length',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
identity_verification: 'Identity verification',
|
identity_verification: 'Identity verification',
|
||||||
@@ -98,6 +99,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
identity_verification: 'Verificación de identidad',
|
identity_verification: 'Verificación de identidad',
|
||||||
@@ -197,6 +199,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
verify_id: 'Verifica ID',
|
verify_id: 'Verifica ID',
|
||||||
identity_verification: "Verifica dell'identità",
|
identity_verification: "Verifica dell'identità",
|
||||||
@@ -296,6 +299,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
identity_verification: 'Identitätsüberprüfung',
|
identity_verification: 'Identitätsüberprüfung',
|
||||||
@@ -395,6 +399,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
must_be_characters_length: 'Doit contenir {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||||
verify_id: "Vérification de l'ID",
|
verify_id: "Vérification de l'ID",
|
||||||
identity_verification: "Vérification de l'identité",
|
identity_verification: "Vérification de l'identité",
|
||||||
@@ -494,6 +499,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
verify_id: 'Zweryfikuj ID',
|
verify_id: 'Zweryfikuj ID',
|
||||||
identity_verification: 'Weryfikacja tożsamości',
|
identity_verification: 'Weryfikacja tożsamości',
|
||||||
@@ -593,6 +599,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
verify_id: 'Підтвердження ідентичності',
|
verify_id: 'Підтвердження ідентичності',
|
||||||
identity_verification: 'Ідентифікація особи',
|
identity_verification: 'Ідентифікація особи',
|
||||||
@@ -692,6 +699,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
verify_id: 'Ověřit ID',
|
verify_id: 'Ověřit ID',
|
||||||
identity_verification: 'Ověření identity',
|
identity_verification: 'Ověření identity',
|
||||||
@@ -791,6 +799,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
identity_verification: 'Verificação de identidade',
|
identity_verification: 'Verificação de identidade',
|
||||||
@@ -890,6 +899,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
verify_id: 'אמת מזהה',
|
verify_id: 'אמת מזהה',
|
||||||
identity_verification: 'אימות זהות',
|
identity_verification: 'אימות זהות',
|
||||||
@@ -989,6 +999,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
verify_id: 'Verifiëren ID',
|
verify_id: 'Verifiëren ID',
|
||||||
identity_verification: 'Identiteitsverificatie',
|
identity_verification: 'Identiteitsverificatie',
|
||||||
@@ -1088,6 +1099,7 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
verify_id: 'تحقق من الهوية',
|
verify_id: 'تحقق من الهوية',
|
||||||
identity_verification: 'التحقق من الهوية',
|
identity_verification: 'التحقق من الهوية',
|
||||||
@@ -1187,6 +1199,7 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
verify_id: '아이디 확인',
|
verify_id: '아이디 확인',
|
||||||
identity_verification: '신원 확인',
|
identity_verification: '신원 확인',
|
||||||
@@ -1286,6 +1299,7 @@ const ko = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ja = {
|
const ja = {
|
||||||
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
verify_id: '本人確認',
|
verify_id: '本人確認',
|
||||||
identity_verification: '本人確認',
|
identity_verification: '本人確認',
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: !!this.previousValue,
|
isInitialsStarted: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false,
|
isDrawInitials: false,
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
@@ -218,6 +218,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.isInitialsStarted = !!this.computedPreviousValue
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -362,11 +365,20 @@ export default {
|
|||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
formData.append('type', 'initials')
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature && !modelValue && !computedPreviousValue"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
@@ -391,7 +391,7 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: false,
|
||||||
isShowQr: false,
|
isShowQr: false,
|
||||||
isOtherReason: false,
|
isOtherReason: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
@@ -422,6 +422,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
this.isSignatureStarted = !!this.computedPreviousValue
|
||||||
|
|
||||||
if (this.requireSigningReason) {
|
if (this.requireSigningReason) {
|
||||||
this.field.preferences ||= {}
|
this.field.preferences ||= {}
|
||||||
this.field.preferences.reason_field_uuid ||= v4()
|
this.field.preferences.reason_field_uuid ||= v4()
|
||||||
@@ -716,11 +718,20 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
formData.append('remember_signature', this.rememberSignature)
|
formData.append('remember_signature', this.rememberSignature)
|
||||||
|
formData.append('type', 'signature')
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
@invalid="validationMessage ? $event.target.setCustomValidity(validationMessage) : ''"
|
||||||
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
@input="validationMessage ? $event.target.setCustomValidity('') : ''"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -127,6 +127,28 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
lengthValidation () {
|
||||||
|
if (this.field.validation?.pattern) {
|
||||||
|
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMessage () {
|
||||||
|
if (this.field.validation?.message) {
|
||||||
|
return this.field.validation.message
|
||||||
|
} else if (this.lengthValidation) {
|
||||||
|
const number =
|
||||||
|
[this.lengthValidation.min, this.lengthValidation.max]
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((v, i, a) => a.indexOf(v) === i)
|
||||||
|
.join('-')
|
||||||
|
|
||||||
|
return this.t('must_be_characters_length').replace('{number}', number)
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -1052,6 +1052,12 @@ export default {
|
|||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: this.defaultDateFormat
|
format: this.defaultDateFormat
|
||||||
@@ -1450,6 +1456,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (['stamp', 'heading'].includes(field.type)) {
|
if (['stamp', 'heading'].includes(field.type)) {
|
||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,13 +153,13 @@
|
|||||||
<option
|
<option
|
||||||
v-for="(key, value) in validations"
|
v-for="(key, value) in validations"
|
||||||
:key="key"
|
:key="key"
|
||||||
:selected="field.validation?.pattern ? value === field.validation.pattern : value === 'none'"
|
:selected="lengthValidation ? key == 'length' : (field.validation?.pattern ? value === field.validation.pattern : key === 'none')"
|
||||||
:value="value"
|
:value="key"
|
||||||
>
|
>
|
||||||
{{ t(key) }}
|
{{ t(key) }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
:selected="field.validation && !validations[field.validation.pattern]"
|
:selected="field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
:value="validations[field.validation?.pattern] || !field.validation?.pattern ? 'custom' : field.validation?.pattern"
|
:value="validations[field.validation?.pattern] || !field.validation?.pattern ? 'custom' : field.validation?.pattern"
|
||||||
>
|
>
|
||||||
{{ t('custom') }}
|
{{ t('custom') }}
|
||||||
@@ -174,7 +174,51 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern]"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
||||||
|
class="py-1.5 px-1 relative flex space-x-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('min')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:value="lengthValidation.min"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="lengthValidation.min"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('min') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('max')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
:value="lengthValidation.max"
|
||||||
|
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="lengthValidation.max"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('max') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -195,6 +239,27 @@
|
|||||||
{{ t('regexp_validation') }}
|
{{ t('regexp_validation') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.validation.message"
|
||||||
|
:placeholder="t('error_message')"
|
||||||
|
dir="auto"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation.message"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('error_message') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'date'"
|
v-if="field.type === 'date'"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -330,7 +395,7 @@
|
|||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="['text', 'number', 'date'].includes(field.type)">
|
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -489,6 +554,7 @@ export default {
|
|||||||
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
selectedValidation: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -533,8 +599,16 @@ export default {
|
|||||||
|
|
||||||
return formats
|
return formats
|
||||||
},
|
},
|
||||||
|
lengthValidation () {
|
||||||
|
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||||
|
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
validations () {
|
validations () {
|
||||||
return {
|
return {
|
||||||
|
'.{0,}': 'length',
|
||||||
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'ssn',
|
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'ssn',
|
||||||
'^[0-9]{2}-[0-9]{7}$': 'ein',
|
'^[0-9]{2}-[0-9]{7}$': 'ein',
|
||||||
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
||||||
@@ -553,13 +627,21 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onChangeValidation (event) {
|
onChangeValidation (event) {
|
||||||
if (event.target.value === 'custom') {
|
if (event.target.value === 'custom') {
|
||||||
this.field.validation = { pattern: '' }
|
this.selectedValidation = 'custom'
|
||||||
|
|
||||||
|
this.field.validation = { pattern: '', message: '' }
|
||||||
|
|
||||||
this.$nextTick(() => this.$refs.validationCustom.focus())
|
this.$nextTick(() => this.$refs.validationCustom.focus())
|
||||||
} else if (event.target.value) {
|
} else if (event.target.value) {
|
||||||
this.field.validation ||= {}
|
this.field.validation ||= {}
|
||||||
this.field.validation.pattern = event.target.value
|
this.field.validation.pattern =
|
||||||
|
Object.keys(this.validations).find(key => this.validations[key] === event.target.value)
|
||||||
|
|
||||||
|
this.selectedValidation = event.target.value
|
||||||
|
delete this.field.validation.message
|
||||||
} else {
|
} else {
|
||||||
|
this.selectedValidation = ''
|
||||||
|
|
||||||
delete this.field.validation
|
delete this.field.validation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
date: this.t('date'),
|
date: this.t('date'),
|
||||||
|
datenow: this.t('date_signed'),
|
||||||
number: this.t('number'),
|
number: this.t('number'),
|
||||||
image: this.t('image'),
|
image: this.t('image'),
|
||||||
file: this.t('file'),
|
file: this.t('file'),
|
||||||
@@ -142,6 +143,7 @@ export default {
|
|||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
datenow: IconCalendarCheck,
|
||||||
number: IconSquareNumber1,
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
@@ -164,7 +166,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
error_message: 'Error message',
|
||||||
|
length: 'Length',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
font: 'Font',
|
font: 'Font',
|
||||||
party: 'Party',
|
party: 'Party',
|
||||||
|
date_signed: 'Date Signed',
|
||||||
method: 'Method',
|
method: 'Method',
|
||||||
reorder_fields: 'Reorder fields',
|
reorder_fields: 'Reorder fields',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
@@ -167,6 +172,11 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
error_message: 'Mensaje de error',
|
||||||
|
length: 'Longitud',
|
||||||
|
min: 'Mín',
|
||||||
|
max: 'Máx',
|
||||||
|
date_signed: 'Fecha actual',
|
||||||
fuente: 'Fuente',
|
fuente: 'Fuente',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -335,6 +345,11 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
error_message: 'Messaggio di errore',
|
||||||
|
length: 'Lunghezza',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_signed: 'Data attuale',
|
||||||
font: 'Carattere',
|
font: 'Carattere',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Metodo',
|
method: 'Metodo',
|
||||||
@@ -503,6 +518,11 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
error_message: 'Mensagem de erro',
|
||||||
|
length: 'Comprimento',
|
||||||
|
min: 'Mín',
|
||||||
|
max: 'Máx',
|
||||||
|
date_signed: 'Data atual',
|
||||||
fonte: 'Fonte',
|
fonte: 'Fonte',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -671,6 +691,11 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
error_message: 'Message d\'erreur',
|
||||||
|
length: 'Longueur',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_signed: 'Date actuelle',
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
party: 'Partie',
|
party: 'Partie',
|
||||||
method: 'Méthode',
|
method: 'Méthode',
|
||||||
@@ -839,6 +864,11 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
error_message: 'Fehlermeldung',
|
||||||
|
length: 'Länge',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_now: 'Akt. Datum',
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
party: 'Partei',
|
party: 'Partei',
|
||||||
method: 'Verfahren',
|
method: 'Verfahren',
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export default {
|
|||||||
this.mappings.every((m) => m.column_index !== index)
|
this.mappings.every((m) => m.column_index !== index)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (columnIndex !== -1) {
|
if (columnIndex !== -1 && this.rows.some((row) => row[columnIndex])) {
|
||||||
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
|
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ class ProcessSubmissionExpiredJob
|
|||||||
return if submission.submitters.where.not(declined_at: nil).exists?
|
return if submission.submitters.where.not(declined_at: nil).exists?
|
||||||
return unless submission.submitters.exists?(completed_at: nil)
|
return unless submission.submitters.exists?(completed_at: nil)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submission.account_id, %w[submission.expired]).each do |webhook|
|
WebhookUrls.enqueue_events(submission, 'submission.expired')
|
||||||
SendSubmissionExpiredWebhookRequestJob.perform_async('submission_id' => submission.id,
|
|
||||||
'webhook_url_id' => webhook.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -63,18 +63,26 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||||
|
event_uuids = {}
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
||||||
if webhook.events.include?('form.completed')
|
if webhook.events.include?('form.completed')
|
||||||
|
event_uuids['form.completed'] ||= SecureRandom.uuid
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'event_uuid' => event_uuids['form.completed'],
|
||||||
'webhook_url_id' => webhook.id)
|
'webhook_url_id' => webhook.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
if webhook.events.include?('submission.completed') && is_all_completed
|
next unless webhook.events.include?('submission.completed') && is_all_completed
|
||||||
|
|
||||||
|
event_uuids['submission.completed'] ||= SecureRandom.uuid
|
||||||
|
|
||||||
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||||
|
'event_uuid' => event_uuids['submission.completed'],
|
||||||
'webhook_url_id' => webhook.id)
|
'webhook_url_id' => webhook.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def enqueue_completed_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ReindexAllSearchEntriesJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform
|
||||||
|
SearchEntries.reindex_all
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,6 +20,9 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submitter,
|
||||||
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submitter,
|
||||||
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ class SendFormStartedWebhookRequestJob
|
|||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submitter,
|
||||||
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ class SendFormViewedWebhookRequestJob
|
|||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submitter,
|
||||||
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submission,
|
||||||
|
attempt:,
|
||||||
data: submission.as_json(only: %i[id archived_at]))
|
data: submission.as_json(only: %i[id archived_at]))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submission,
|
||||||
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submission,
|
||||||
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ class SendSubmissionExpiredWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.expired')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.expired')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.expired',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.expired',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: submission,
|
||||||
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||||
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
'submission_id' => submission.id,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: template,
|
||||||
|
attempt:,
|
||||||
data: Templates::SerializeForApi.call(template))
|
data: Templates::SerializeForApi.call(template))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
|
||||||
|
|
||||||
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: template,
|
||||||
|
attempt:,
|
||||||
data: Templates::SerializeForApi.call(template))
|
data: Templates::SerializeForApi.call(template))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!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,
|
**params,
|
||||||
'webhook_url_id' => webhook_url.id,
|
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
'last_status' => resp&.status.to_i
|
'last_status' => resp&.status.to_i
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendTestWebhookRequestJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url && submitter
|
||||||
|
|
||||||
|
Faraday.post(webhook_url.url,
|
||||||
|
{
|
||||||
|
event_type: 'form.completed',
|
||||||
|
timestamp: Time.current.iso8601,
|
||||||
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT,
|
||||||
|
**webhook_url.secret.to_h)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :account_linked_accounts, dependent: :destroy
|
has_many :account_linked_accounts, dependent: :destroy
|
||||||
has_many :email_events, dependent: :destroy
|
has_many :email_events, dependent: :destroy
|
||||||
has_many :webhook_urls, dependent: :destroy
|
has_many :webhook_urls, dependent: :destroy
|
||||||
|
has_many :webhook_events, dependent: nil
|
||||||
has_many :account_accesses, dependent: :destroy
|
has_many :account_accesses, dependent: :destroy
|
||||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||||
class_name: 'AccountLinkedAccount',
|
class_name: 'AccountLinkedAccount',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#
|
#
|
||||||
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||||
# index_email_events_on_email (email)
|
# index_email_events_on_email (email)
|
||||||
|
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
||||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||||
# index_email_events_on_message_id (message_id)
|
# index_email_events_on_message_id (message_id)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submissions_on_account_id_and_id (account_id,id)
|
# index_submissions_on_account_id_and_id (account_id,id)
|
||||||
|
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
|
||||||
|
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
# index_submissions_on_slug (slug) UNIQUE
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_template_id (template_id)
|
||||||
@@ -38,7 +40,7 @@ class Submission < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
|
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
|
||||||
|
|
||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class Submitter < ApplicationRecord
|
|||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :template, through: :submission
|
has_one :template, through: :submission
|
||||||
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
|
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :preferences, :string, default: -> { {} }
|
attribute :preferences, :string, default: -> { {} }
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_templates_on_account_id (account_id)
|
# index_templates_on_account_id (account_id)
|
||||||
|
# index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE (archived_at IS NULL)
|
||||||
|
# index_templates_on_account_id_and_id_archived (account_id,id) WHERE (archived_at IS NOT NULL)
|
||||||
# index_templates_on_author_id (author_id)
|
# index_templates_on_author_id (author_id)
|
||||||
# index_templates_on_external_id (external_id)
|
# index_templates_on_external_id (external_id)
|
||||||
# index_templates_on_folder_id (folder_id)
|
# index_templates_on_folder_id (folder_id)
|
||||||
@@ -42,7 +44,7 @@ class Template < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :folder, class_name: 'TemplateFolder'
|
belongs_to :folder, class_name: 'TemplateFolder'
|
||||||
|
|
||||||
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy
|
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
|
||||||
|
|
||||||
before_validation :maybe_set_default_folder, on: :create
|
before_validation :maybe_set_default_folder, on: :create
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: webhook_attempts
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# attempt :integer not null
|
||||||
|
# response_body :text
|
||||||
|
# response_status_code :integer not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# webhook_event_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_webhook_attempts_on_webhook_event_id (webhook_event_id)
|
||||||
|
#
|
||||||
|
class WebhookAttempt < ApplicationRecord
|
||||||
|
belongs_to :webhook_event
|
||||||
|
|
||||||
|
def success?
|
||||||
|
[2, 3].include?(response_status_code.to_i / 100)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: webhook_events
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# event_type :string not null
|
||||||
|
# record_type :string not null
|
||||||
|
# status :string not null
|
||||||
|
# uuid :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# record_id :bigint not null
|
||||||
|
# webhook_url_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_webhook_events_error (webhook_url_id,id) WHERE ((status)::text = 'error'::text)
|
||||||
|
# index_webhook_events_on_uuid_and_webhook_url_id (uuid,webhook_url_id) UNIQUE
|
||||||
|
# index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id)
|
||||||
|
#
|
||||||
|
class WebhookEvent < ApplicationRecord
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
belongs_to :webhook_url, optional: true
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
belongs_to :record, polymorphic: true, optional: true
|
||||||
|
|
||||||
|
has_many :webhook_attempts, dependent: nil
|
||||||
|
end
|
||||||
@@ -37,6 +37,7 @@ class WebhookUrl < ApplicationRecord
|
|||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
has_many :webhook_events, dependent: nil
|
||||||
|
|
||||||
attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
|
attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
|
||||||
attribute :secret, :string, default: -> { {} }
|
attribute :secret, :string, default: -> { {} }
|
||||||
|
|||||||
Whitespace-only changes.
@@ -14,7 +14,10 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :timezone, t('time_zone'), class: 'label' %>
|
<%= ff.label :timezone, t('time_zone'), class: 'label' %>
|
||||||
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
||||||
<%= time_zone_options_for_select(current_account.timezone) %>
|
<% tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[current_account.timezone] || current_account.timezone) %>
|
||||||
|
<% items = ActiveSupport::TimeZone.all.map { |z| [z.to_s, z.name] } %>
|
||||||
|
<% items.unshift([tzinfo.to_s, current_account.timezone]) unless ActiveSupport::TimeZone.all.find { |e| e.tzinfo == tzinfo } %>
|
||||||
|
<%= options_for_select(items, current_account.timezone) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -165,6 +168,15 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render 'extra_preferences' %>
|
||||||
|
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
Efficient search with search index
|
||||||
|
</span>
|
||||||
|
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'compliances' %>
|
<%= render 'compliances' %>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="us_server_selector" class="flex justify-center hidden">
|
<div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
|
||||||
<% if eu_server %>
|
<% if eu_server %>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.classList.add('text-[0.7vw]', 'lg:text-[0.45rem]');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get field() {
|
get field() {
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
if (!window.customElements.get('server-selector')) {
|
if (!window.customElements.get('server-selector')) {
|
||||||
customElements.define('server-selector', class extends HTMLElement {
|
customElements.define('server-selector', class extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const usServerSelector = this.querySelector('#us_server_selector');
|
const serverSelector = this.querySelector('#server_selector');
|
||||||
const globalServerSelector = this.querySelector('#global_server_selector');
|
const globalServerSelector = this.querySelector('#global_server_selector');
|
||||||
const euServerAlert = this.querySelector('#eu_server_alert');
|
const euServerAlert = this.querySelector('#eu_server_alert');
|
||||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
|
||||||
|
|
||||||
|
if (!serverSelector.classList.contains('hidden')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (usTimezones.test(timezone)) {
|
if (usTimezones.test(timezone)) {
|
||||||
usServerSelector.classList.remove('hidden');
|
serverSelector.classList.remove('hidden');
|
||||||
} else if (timezone.includes('Europe')) {
|
} else if (timezone.includes('Europe')) {
|
||||||
globalServerSelector.classList.remove('hidden');
|
globalServerSelector.classList.remove('hidden');
|
||||||
euServerAlert?.classList?.remove('hidden');
|
euServerAlert?.classList?.remove('hidden');
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
<% link = pagy_anchor(@pagy) %>
|
<% link = pagy_anchor(pagy) %>
|
||||||
<% if @pagy.pages > 1 %>
|
<% if pagy.pages > 1 %>
|
||||||
<div class="flex my-6 justify-center md:justify-between">
|
<div class="flex my-6 justify-center md:justify-between">
|
||||||
<div class="hidden md:block text-sm">
|
<div class="hidden md:block text-sm">
|
||||||
<% if @pagy.count.nil? %>
|
<% if pagy.count.nil? %>
|
||||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %>
|
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %>
|
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= local_assigns[:left_additional_html] %>
|
<%= local_assigns[:left_additional_html] %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1.5">
|
<div class="flex items-center space-x-1.5">
|
||||||
<%= local_assigns[:right_additional_html] %>
|
<%= local_assigns[:right_additional_html] %>
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<% if @pagy.prev %>
|
<% if pagy.prev %>
|
||||||
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
<%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||||
<%= t('page_number', number: @pagy.page) %>
|
<%= t('page_number', number: pagy.page) %>
|
||||||
</span>
|
</span>
|
||||||
<% if @pagy.next %>
|
<% if local_assigns[:next_page_path].present? %>
|
||||||
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
<%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
|
||||||
|
<% elsif pagy.next %>
|
||||||
|
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %>
|
|
||||||
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
|
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
|
||||||
<label tabindex="0" class="btn btn-sm h-10">
|
<label tabindex="0" class="btn btn-sm h-10">
|
||||||
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
|
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
|
||||||
@@ -6,7 +5,7 @@
|
|||||||
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
|
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
|
||||||
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
|
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
|
||||||
<li>
|
<li>
|
||||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'created_at' %>">
|
<button class="<%= 'bg-base-200' if selected_order == 'created_at' %>">
|
||||||
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
|
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
|
||||||
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
|
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
<% if local_assigns[:with_recently_used] != false %>
|
<% if local_assigns[:with_recently_used] != false %>
|
||||||
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
|
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
|
||||||
<li>
|
<li>
|
||||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'used_at' %>">
|
<button class="<%= 'bg-base-200' if selected_order == 'used_at' %>">
|
||||||
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
|
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
|
||||||
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
|
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -24,7 +23,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
|
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
|
||||||
<li>
|
<li>
|
||||||
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'name' %>">
|
<button class="<%= 'bg-base-200' if selected_order == 'name' %>">
|
||||||
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
|
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
|
||||||
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
|
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
×
|
×
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="w-full md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
|
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
|
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
|
||||||
<toggle-submit class="block">
|
<toggle-submit class="block">
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
||||||
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
|
||||||
<div class="space-y-6 mx-auto">
|
<% end %>
|
||||||
<div class="space-y-6">
|
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<%= render 'banner' %>
|
<%= render 'banner' %>
|
||||||
<p class="text-xl font-semibold text-center">
|
<p class="text-xl font-semibold text-center">
|
||||||
@@ -22,7 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% if can?(:update, @template) %>
|
||||||
</div>
|
<toggle-submit class="block">
|
||||||
|
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
||||||
<% content_for(:html_description, "#{@template.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %>
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||||
|
<% end %>
|
||||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -27,13 +29,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<% if !@template.archived_at? && !@template.account.archived_at? %>
|
<% if !@template.archived_at? && !@template.account.archived_at? %>
|
||||||
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
||||||
|
<% if @error_message %>
|
||||||
|
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
|
||||||
|
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
||||||
|
<div class="text-sm"><%= @error_message %></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
|
||||||
|
<% multiple_fields = link_form_fields.size > 1 %>
|
||||||
|
<% if link_form_fields.include?('name') %>
|
||||||
|
<div dir="auto" class="form-control !mt-0">
|
||||||
|
<%= f.label :name, t('name'), class: 'label' %>
|
||||||
|
<%= f.text_field :name, value: current_user&.full_name || params[:name] || @submitter.name, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_name' : 'provide_your_name_to_start') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if link_form_fields.include?('email') %>
|
||||||
<div dir="auto" class="form-control !mt-0">
|
<div dir="auto" class="form-control !mt-0">
|
||||||
<%= f.label :email, t('email'), class: 'label' %>
|
<%= f.label :email, t('email'), class: 'label' %>
|
||||||
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %>
|
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_email' : 'provide_your_email_to_start') %>
|
||||||
<% if @error_message %>
|
|
||||||
<span class="label-text-alt text-red-400 mt-1"><%= @error_message %></span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if link_form_fields.include?('phone') %>
|
||||||
|
<div dir="auto" class="form-control !mt-0">
|
||||||
|
<%= f.label :phone, t('phone'), class: 'label' %>
|
||||||
|
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<toggle-submit dir="auto" class="form-control">
|
<toggle-submit dir="auto" class="form-control">
|
||||||
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
|
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="name">
|
<submitters-autocomplete data-field="name">
|
||||||
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero?, value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
|
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero?, value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
|
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% if submitters.size > 1 %>
|
<% if submitters.size > 1 %>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'initials', 'stamp']) %>
|
<% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment']) %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="px-0.5 flex flex-col justify-center">
|
<div class="px-0.5 flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
|
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
|
||||||
<% schema.each do |item| %>
|
<% schema.each do |item| %>
|
||||||
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
|
<% if document.preview_images.first %>
|
||||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||||
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||||
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div class="pr-3.5 pl-0.5">
|
<div class="pr-3.5 pl-0.5">
|
||||||
@@ -92,7 +94,7 @@
|
|||||||
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.first.metadata %>
|
<% lazyload_metadata = document.preview_images.first&.metadata || {} %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
@@ -242,9 +244,9 @@
|
|||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'stamp']) %>
|
<% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
|
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
|
||||||
<% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %>
|
<% I18n.with_locale(@submitter.account.locale) do %>
|
||||||
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
|
||||||
|
<% end %>
|
||||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||||
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
|
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% templates_order_select_html = capture do %>
|
<% templates_order_select_html = capture do %>
|
||||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% templates_order_select_html = capture do %>
|
<% templates_order_select_html = capture do %>
|
||||||
<% if params[:q].blank? && @pagy.pages > 1 %>
|
<% if params[:q].blank? && @pagy.pages > 1 %>
|
||||||
<%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
|
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @template_folders.present? %>
|
<% if @template_folders.present? %>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<% if @templates.present? %>
|
<% if @templates.present? %>
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
<%= render partial: 'templates/template', collection: @templates %>
|
<%= render partial: 'templates/template', collection: @templates %>
|
||||||
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %>
|
<% if (show_dropzone && current_user.created_at > 2.weeks.ago && current_user == true_user) || params[:tour] == 'true' %>
|
||||||
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
|
||||||
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
@@ -98,5 +98,14 @@
|
|||||||
<%= t('templates_not_found') %>
|
<%= t('templates_not_found') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @related_submissions.present? %>
|
||||||
|
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
|
||||||
|
<%= t('submissions') %>
|
||||||
|
</h1>
|
||||||
|
<div class="space-y-4 mt-4">
|
||||||
|
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/review_form' %>
|
<%= render 'shared/review_form' %>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
|
||||||
|
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
|
||||||
|
<% unless close_on_submit %>
|
||||||
|
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
|
||||||
|
<% end %>
|
||||||
|
<div class="space-y-3 divide-y">
|
||||||
|
<% template.submitters.each_with_index do |submitter, index| %>
|
||||||
|
<div class="<%= 'pt-3' if index.positive? %>">
|
||||||
|
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
|
||||||
|
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
|
||||||
|
<%= ff.hidden_field :uuid %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
|
||||||
|
<% if template.submitters.size == 2 %>
|
||||||
|
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
||||||
|
<% else %>
|
||||||
|
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
||||||
|
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
|
||||||
|
</toggle-attribute>
|
||||||
|
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% if template.submitters.size == 2 %>
|
||||||
|
<checkbox-group class="mt-3 flex items-center space-x-4">
|
||||||
|
<% if local_assigns[:with_submission_requester] != false %>
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
||||||
|
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
|
||||||
|
</toggle-attribute>
|
||||||
|
<span class="select-none">
|
||||||
|
<%= t('submission_requester') %>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<% if index == 1 %>
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
||||||
|
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
|
||||||
|
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, template.submitters.first['uuid'], '' %>
|
||||||
|
</indeterminate-checkbox>
|
||||||
|
</toggle-attribute>
|
||||||
|
<span class="select-none">
|
||||||
|
<%= t('invite_by_name', name: template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
</checkbox-group>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if local_assigns[:with_toggles] != false %>
|
||||||
|
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
|
||||||
|
<%= form_for template, url: template_preferences_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
|
||||||
|
<span>
|
||||||
|
<%= t('enforce_recipients_order') %>
|
||||||
|
</span>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(template.preferences['submitters_order']) do |ff| %>
|
||||||
|
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:manage, :personalization_advanced) %>
|
||||||
|
<%= form_for template, url: template_preferences_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<div class="flex items-center mt-4 justify-between w-full">
|
||||||
|
<span>
|
||||||
|
<%= t('ensure_unique_recipients') %>
|
||||||
|
</span>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(template.preferences['validate_unique_submitters']) do |ff| %>
|
||||||
|
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control mt-5">
|
||||||
|
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
|
||||||
|
<% unless close_on_submit %>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<span id="form_saved_alert" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -287,81 +287,8 @@
|
|||||||
<%= render 'templates_code_modal/preferences', class: 'pt-2' %>
|
<%= render 'templates_code_modal/preferences', class: 'pt-2' %>
|
||||||
</div>
|
</div>
|
||||||
<% if show_recipients %>
|
<% if show_recipients %>
|
||||||
<div id="recipients" class="hidden mt-2 mb-4 px-5">
|
<div id="recipients" class="hidden mt-2 mb-4 px-5 pt-1 pb-2">
|
||||||
<%= form_for @template, url: template_recipients_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form } do |f| %>
|
<%= render 'recipients', template: @template %>
|
||||||
<div class="space-y-3 divide-y">
|
|
||||||
<% @template.submitters.each_with_index do |submitter, index| %>
|
|
||||||
<div class="pt-3">
|
|
||||||
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
|
|
||||||
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
|
|
||||||
<%= ff.hidden_field :uuid %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
|
|
||||||
<% if @template.submitters.size == 2 %>
|
|
||||||
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
|
||||||
<% else %>
|
|
||||||
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
|
||||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
|
|
||||||
</toggle-attribute>
|
|
||||||
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% if @template.submitters.size == 2 %>
|
|
||||||
<checkbox-group class="mt-3 flex items-center space-x-4">
|
|
||||||
<label class="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
|
||||||
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
|
|
||||||
</toggle-attribute>
|
|
||||||
<span class="select-none">
|
|
||||||
<%= t('submission_requester') %>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<% if index == 1 %>
|
|
||||||
<label class="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
|
||||||
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
|
|
||||||
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, @template.submitters.first['uuid'], '' %>
|
|
||||||
</indeterminate-checkbox>
|
|
||||||
</toggle-attribute>
|
|
||||||
<span class="select-none">
|
|
||||||
<%= t('invite_by_name', name: @template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
</checkbox-group>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
|
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
|
||||||
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
|
|
||||||
<span>
|
|
||||||
<%= t('enforce_recipients_order') %>
|
|
||||||
</span>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
|
|
||||||
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if can?(:manage, :personalization_advanced) %>
|
|
||||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
|
|
||||||
<div class="flex items-center mt-4 justify-between w-full">
|
|
||||||
<span>
|
|
||||||
<%= t('ensure_unique_recipients') %>
|
|
||||||
</span>
|
|
||||||
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(@template.preferences['validate_unique_submitters']) do |ff| %>
|
|
||||||
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control mt-5 pb-2">
|
|
||||||
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if show_api %>
|
<% if show_api %>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
|
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
|
||||||
<div class="mt-2 mb-4 px-5">
|
<div class="mt-2 mb-4 px-5">
|
||||||
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
||||||
<div class="flex items-center justify-between gap-1 px-1">
|
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
|
||||||
<span><%= t('enable_shared_link') %></span>
|
<span><%= t('enable_shared_link') %></span>
|
||||||
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
|
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
|
||||||
</div>
|
</label>
|
||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
|
||||||
<check-on-click data-element-id="template_shared_link">
|
<check-on-click data-element-id="template_shared_link">
|
||||||
@@ -12,5 +12,34 @@
|
|||||||
</check-on-click>
|
</check-on-click>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
|
||||||
|
<label class="pl-1"><%= t('link_form_fields') %></label>
|
||||||
|
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
|
||||||
|
<% %w[name email phone].each do |field| %>
|
||||||
|
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
|
||||||
|
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
|
||||||
|
<span class="font-medium w-full text-center"><%= t(field) %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</required-checkbox-group>
|
||||||
|
<% end %>
|
||||||
|
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
|
||||||
|
<div class="alert items-start bg-base-100 border-base-300 mt-4">
|
||||||
|
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
||||||
|
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @template.submitters.to_a.length > 1 %>
|
||||||
|
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5">
|
||||||
|
<input type="checkbox" name="accordion">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<%= t('default_parties') %>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content !pb-0">
|
||||||
|
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<div id="drawer_events_<%= dom_id(webhook_event) %>">
|
||||||
|
<ol class="relative border-s border-base-300 space-y-6 ml-3">
|
||||||
|
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
|
||||||
|
<% if webhook_event.status == 'error' %>
|
||||||
|
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
|
||||||
|
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %>
|
||||||
|
<li class="ml-7">
|
||||||
|
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
|
||||||
|
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||||
|
</span>
|
||||||
|
<p class="leading-none text-base-content/90 pt-1">
|
||||||
|
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, last_attempt.created_at + (2**last_attempt.attempt).minutes)) %>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if webhook_attempts.present? %>
|
||||||
|
<% webhook_attempts.each do |webhook_attempt| %>
|
||||||
|
<li class="ml-7">
|
||||||
|
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center <%= webhook_attempt.success? ? 'btn-success bg-lime-50' : 'btn-error bg-red-50' %>" style="left: -12px;">
|
||||||
|
<%= svg_icon(webhook_attempt.success? ? 'check' : 'x', class: 'w-4 h-4 shrink-0') %>
|
||||||
|
</span>
|
||||||
|
<p class="leading-none text-sm text-base-content/60 pt-1">
|
||||||
|
<%= l(webhook_attempt.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||||
|
</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm font-bold text-base-content/80">
|
||||||
|
<span><%= Rack::Utils::HTTP_STATUS_CODES[webhook_attempt.response_status_code] %></span>
|
||||||
|
<% if webhook_attempt.response_status_code.positive? %>
|
||||||
|
<span>(<%= webhook_attempt.response_status_code %>)</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% unless webhook_attempt.success? %>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">
|
||||||
|
<%= webhook_attempt.response_body.presence || Rack::Utils::HTTP_STATUS_CODES[webhook_attempt.response_status_code] %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<li class="ml-7">
|
||||||
|
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center btn-info bg-blue-50" style="left: -12px;">
|
||||||
|
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||||
|
</span>
|
||||||
|
<p class="leading-none text-base-content/60 pt-1">
|
||||||
|
<%= l(webhook_event.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ol>
|
||||||
|
<% unless webhook_event.status == 'pending' %>
|
||||||
|
<div class="absolute right-4 top-3">
|
||||||
|
<%= button_to button_title(title: t('resend'), disabled_with: t('awaiting'), icon: svg_icon('rotate', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), resend_settings_webhook_event_path(webhook_url.id, webhook_event.uuid), form: { id: button_uuid = SecureRandom.uuid }, params: { button_id: button_uuid }, class: 'btn btn-neutral btn-sm text-white', method: :post, onclick: '[this.form.requestSubmit(), this.disabled = true]' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div id="<%= dom_id(webhook_event) %>" class="group relative hover:cursor-pointer hover:bg-base-200">
|
||||||
|
<a href="<%= settings_webhook_event_path(webhook_url.id, webhook_event.uuid) %>" data-turbo-frame="drawer" class="top-0 bottom-0 left-0 right-0 absolute"></a>
|
||||||
|
<div class="min-h-12 flex flex-col md:flex-row md:items-center md:justify-between gap-2 px-3 py-2">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<% if webhook_event.status == 'success' %>
|
||||||
|
<div class="btn btn-outline btn-xs btn-success bg-lime-50 gap-1">
|
||||||
|
<%= svg_icon('check', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||||
|
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||||
|
</div>
|
||||||
|
<% elsif webhook_event.status == 'pending' %>
|
||||||
|
<div class="btn btn-outline btn-xs btn-info bg-blue-50 gap-1">
|
||||||
|
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0 stroke-2') %>
|
||||||
|
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||||
|
</div>
|
||||||
|
<% elsif webhook_event.status == 'error' %>
|
||||||
|
<div class="btn btn-outline btn-xs btn-error bg-red-50 gap-1">
|
||||||
|
<%= svg_icon('x', class: 'w-4 h-4 shrink-0') %>
|
||||||
|
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div><%= webhook_event.event_type %></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<%= button_to button_title(title: t('resend'), disabled_with: t('awaiting'), icon: svg_icon('rotate', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), resend_settings_webhook_event_path(webhook_url.id, webhook_event.uuid), form: { id: button_uuid = SecureRandom.uuid }, params: { button_id: button_uuid }, class: 'btn btn-neutral btn-xs h-2 text-white relative z-[1] hidden md:group-hover:inline-block', data: { turbo_frame: :drawer }, method: :post, onclick: "[this.form.requestSubmit(), this.disabled = true, this.classList.remove('hidden')]" %>
|
||||||
|
<span><%= l(webhook_event.created_at, locale: current_account.locale, format: :short) %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<%= render 'shared/turbo_drawer', title: @webhook_event.event_type, close_after_submit: false do %>
|
||||||
|
<div class="relative px-4 py-4">
|
||||||
|
<%= render 'drawer_events', webhook_url: @webhook_url, webhook_event: @webhook_event %>
|
||||||
|
<% if @data %>
|
||||||
|
<div class="mockup-code overflow-hidden relative pb-0 mt-6">
|
||||||
|
<% response = JSON.pretty_generate({ event_type: @webhook_event.event_type, timestamp: @webhook_event.created_at.as_json, data: @data }) %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: response, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
</span>
|
||||||
|
<pre class="before:!m-0 pl-4 pb-4"><code class="overflow-hidden text-sm w-full"><%== HighlightCode.call(response, 'JSON', theme: 'base16.dark') %></code></pre>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -79,8 +79,49 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
<% if @webhook_events.present? || params[:status].present? %>
|
||||||
<% if submitter %>
|
<div class="mt-6">
|
||||||
|
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
|
||||||
|
<div class="tabs border-b mt-4">
|
||||||
|
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
|
<%= link_to t('successed'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
|
<%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
|
</div>
|
||||||
|
<% if @webhook_events.present? %>
|
||||||
|
<div class="divide-y divide-base-300 rounded-lg">
|
||||||
|
<%= render partial: 'webhook_events/event_row', collection: @webhook_events, as: :webhook_event, locals: { webhook_url: @webhook_url } %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<%= t('there_are_no_events') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @pagy.pages > 1 %>
|
||||||
|
<div class="flex my-4 justify-center md:justify-between">
|
||||||
|
<div class="hidden md:block text-sm">
|
||||||
|
<%= "#{@pagy.from}-#{@pagy.to} events" %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<div class="join">
|
||||||
|
<% if @pagy.prev %>
|
||||||
|
<%= link_to '«', url_for(page: @pagy.prev, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||||
|
<% else %>
|
||||||
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||||
|
<% end %>
|
||||||
|
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||||
|
<%= "Page #{@pagy.page}" %>
|
||||||
|
</span>
|
||||||
|
<% if @pagy.next %>
|
||||||
|
<%= link_to '»', url_for(page: @pagy.next, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||||
|
<% else %>
|
||||||
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% elsif (submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last) && can?(:read, submitter) %>
|
||||||
<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">
|
||||||
@@ -98,7 +139,7 @@
|
|||||||
<span class="top-0 right-0 absolute">
|
<span class="top-0 right-0 absolute">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.completed', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.completed', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</span>
|
</span>
|
||||||
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
<pre><code class="overflow-hidden w-full"><%== HighlightCode.call(code, 'JSON', theme: 'base16.dark') %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ignored_warnings": [
|
"ignored_warnings": [
|
||||||
{
|
{
|
||||||
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
|
"fingerprint": "bbd1bdad94998e53a48921859065e06cd1595502d4dc40362afcaa90307b591a",
|
||||||
"note": "Permitted parameters are necessary for creating submitters via API"
|
"note": "Permitted parameters are necessary for creating submitters via API"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
{
|
{
|
||||||
"fingerprint": "5f52190d03ee922bba9792012d8fcbeb7d4736006bb899b3be9cc10d679e0af1",
|
"fingerprint": "5f52190d03ee922bba9792012d8fcbeb7d4736006bb899b3be9cc10d679e0af1",
|
||||||
"note": "Safe Param"
|
"note": "Safe Param"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fingerprint": "dbbfb4a4ace7f43d8247cbb44afa8b628e005e6194ca5552e029b200f725a2d5",
|
||||||
|
"message": "Unescaped find_by!(uuid: params[:id]) is not risky"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Rouge
|
||||||
|
autoload :InheritableHash, 'rouge/util'
|
||||||
|
autoload :Token, 'rouge/token'
|
||||||
|
autoload :Lexer, 'rouge/lexer'
|
||||||
|
autoload :RegexLexer, 'rouge/regex_lexer'
|
||||||
|
|
||||||
|
module Lexers
|
||||||
|
autoload :JSON, 'rouge/lexers/json'
|
||||||
|
end
|
||||||
|
|
||||||
|
autoload :Formatter, 'rouge/formatter'
|
||||||
|
|
||||||
|
module Formatters
|
||||||
|
autoload :HTML, 'rouge/formatters/html'
|
||||||
|
autoload :HTMLInline, 'rouge/formatters/html_inline'
|
||||||
|
end
|
||||||
|
|
||||||
|
autoload :Theme, 'rouge/theme'
|
||||||
|
end
|
||||||
+220
-10
@@ -22,7 +22,10 @@ en: &en
|
|||||||
hi_there: Hi there
|
hi_there: Hi there
|
||||||
thanks: Thanks
|
thanks: Thanks
|
||||||
private: Private
|
private: Private
|
||||||
|
default_parties: Default parties
|
||||||
|
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
||||||
stripe_integration: Stripe Integration
|
stripe_integration: Stripe Integration
|
||||||
|
require_all_recipients: Require all recipients
|
||||||
stripe_account_has_been_connected: Stripe account has been connected.
|
stripe_account_has_been_connected: Stripe account has been connected.
|
||||||
re_connect_stripe: Re-connect Stripe
|
re_connect_stripe: Re-connect Stripe
|
||||||
bcc_recipients: BCC recipients
|
bcc_recipients: BCC recipients
|
||||||
@@ -113,6 +116,11 @@ en: &en
|
|||||||
role: Role
|
role: Role
|
||||||
reason: Reason
|
reason: Reason
|
||||||
provide_your_email_to_start: Provide your email to start
|
provide_your_email_to_start: Provide your email to start
|
||||||
|
provide_your_email: Provide your email
|
||||||
|
provide_your_name_to_start: Provide your name to start
|
||||||
|
provide_your_name: Provide your name
|
||||||
|
provide_your_phone_in_international_format_to_start: Provide your phone in international format to start
|
||||||
|
provide_your_phone_in_international_format: Provide your phone in international format
|
||||||
start: Start
|
start: Start
|
||||||
enforce_recipients_order: Enforce recipients order
|
enforce_recipients_order: Enforce recipients order
|
||||||
starting: Starting
|
starting: Starting
|
||||||
@@ -749,6 +757,18 @@ en: &en
|
|||||||
share_link: Share link
|
share_link: Share link
|
||||||
enable_shared_link: Enable shared link
|
enable_shared_link: Enable shared link
|
||||||
share_link_is_currently_disabled: Share link is currently disabled
|
share_link_is_currently_disabled: Share link is currently disabled
|
||||||
|
select_data_residency: Select data residency
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Review or download completed documents. Fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.
|
||||||
|
link_form_fields: Link form fields
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: At least one field must be displayed in the form.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: This template has multiple parties, which prevents the use of a shared link as it's unclear which party is responsible for specific fields. To resolve this, define the default party details.
|
||||||
|
events_log: Events Log
|
||||||
|
successed: Successed
|
||||||
|
failed: Failed
|
||||||
|
there_are_no_events: There are no events
|
||||||
|
resend: Resend
|
||||||
|
next_attempt_in_time_in_words: Next attempt in %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Bulk Send
|
bulk: Bulk Send
|
||||||
@@ -849,8 +869,14 @@ en: &en
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from}-%{to} of %{count} items"
|
range_with_total: "%{from}-%{to} of %{count} items"
|
||||||
range_without_total: "%{from}-%{to} items"
|
range_without_total: "%{from}-%{to} items"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from}-%{to} of %{count} events"
|
||||||
|
range_without_total: "%{from}-%{to} events"
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
default_parties: Partes predeterminadas
|
||||||
|
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
|
||||||
|
require_all_recipients: Requerir a todos los destinatarios
|
||||||
stripe_integration: Integración con Stripe
|
stripe_integration: Integración con Stripe
|
||||||
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
|
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
|
||||||
re_connect_stripe: Volver a conectar Stripe
|
re_connect_stripe: Volver a conectar Stripe
|
||||||
@@ -946,6 +972,11 @@ es: &es
|
|||||||
role: Rol
|
role: Rol
|
||||||
reason: Razón
|
reason: Razón
|
||||||
provide_your_email_to_start: Proporciona tu correo electrónico para comenzar
|
provide_your_email_to_start: Proporciona tu correo electrónico para comenzar
|
||||||
|
provide_your_email: Proporciona tu correo electrónico
|
||||||
|
provide_your_name_to_start: Proporciona tu nombre para comenzar
|
||||||
|
provide_your_name: Proporciona tu nombre
|
||||||
|
provide_your_phone_in_international_format_to_start: Proporciona tu teléfono en formato internacional para comenzar
|
||||||
|
provide_your_phone_in_international_format: Proporciona tu teléfono en formato internacional
|
||||||
start: Comenzar
|
start: Comenzar
|
||||||
starting: Comenzando
|
starting: Comenzando
|
||||||
form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.'
|
form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.'
|
||||||
@@ -1581,6 +1612,18 @@ es: &es
|
|||||||
share_link: Enlace para compartir
|
share_link: Enlace para compartir
|
||||||
enable_shared_link: Habilitar enlace compartido
|
enable_shared_link: Habilitar enlace compartido
|
||||||
share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
|
share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
|
||||||
|
select_data_residency: Seleccionar ubicación de datos
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} te ha invitado a completar y firmar documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Revisa o descarga los documentos completados. Completa y firma documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.
|
||||||
|
link_form_fields: Vincular campos del formulario
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: Al menos un campo debe mostrarse en el formulario.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Esta plantilla tiene varias partes, lo que impide el uso de un enlace compartido porque no está claro qué parte es responsable de campos específicos. Para resolverlo, define los detalles predeterminados de la parte.
|
||||||
|
events_log: Registro de eventos
|
||||||
|
successed: Exitoso
|
||||||
|
failed: Fallido
|
||||||
|
there_are_no_events: No hay eventos
|
||||||
|
resend: Reenviar
|
||||||
|
next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envío masivo
|
bulk: Envío masivo
|
||||||
@@ -1681,8 +1724,14 @@ es: &es
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from}-%{to} de %{count} elementos"
|
range_with_total: "%{from}-%{to} de %{count} elementos"
|
||||||
range_without_total: "%{from}-%{to} elementos"
|
range_without_total: "%{from}-%{to} elementos"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||||
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
default_parties: Parti predefiniti
|
||||||
|
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
|
||||||
|
require_all_recipients: Richiedi tutti i destinatari
|
||||||
stripe_integration: Integrazione Stripe
|
stripe_integration: Integrazione Stripe
|
||||||
stripe_account_has_been_connected: L'account Stripe è stato collegato.
|
stripe_account_has_been_connected: L'account Stripe è stato collegato.
|
||||||
re_connect_stripe: Ricollega Stripe
|
re_connect_stripe: Ricollega Stripe
|
||||||
@@ -1777,6 +1826,11 @@ it: &it
|
|||||||
role: Ruolo
|
role: Ruolo
|
||||||
reason: Ragione
|
reason: Ragione
|
||||||
provide_your_email_to_start: Fornisci la tua email per iniziare
|
provide_your_email_to_start: Fornisci la tua email per iniziare
|
||||||
|
provide_your_email: Fornisci la tua email
|
||||||
|
provide_your_name_to_start: Fornisci il tuo nome per iniziare
|
||||||
|
provide_your_name: Fornisci il tuo nome
|
||||||
|
provide_your_phone_in_international_format_to_start: Fornisci il tuo telefono in formato internazionale per iniziare
|
||||||
|
provide_your_phone_in_international_format: Fornisci il tuo telefono in formato internazionale
|
||||||
start: Inizia
|
start: Inizia
|
||||||
starting: Iniziando
|
starting: Iniziando
|
||||||
form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.'
|
form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.'
|
||||||
@@ -2411,6 +2465,18 @@ it: &it
|
|||||||
share_link: Link di condivisione
|
share_link: Link di condivisione
|
||||||
enable_shared_link: Abilita link condiviso
|
enable_shared_link: Abilita link condiviso
|
||||||
share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
|
share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
|
||||||
|
select_data_residency: Seleziona la residenza dei dati
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} ti ha invitato a compilare e firmare documenti online con facilità utilizzando una soluzione di firma digitale sicura, veloce e facile da usare.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Rivedi o scarica i documenti completati. Compila e firma documenti online facilmente con una soluzione di firma digitale sicura, veloce e facile da usare.
|
||||||
|
link_form_fields: Collega i campi del modulo
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: Almeno un campo deve essere visualizzato nel modulo.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Questo modello ha più parti, il che impedisce l’uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte.
|
||||||
|
events_log: Registro eventi
|
||||||
|
successed: Riuscito
|
||||||
|
failed: Fallito
|
||||||
|
there_are_no_events: Nessun evento
|
||||||
|
resend: Invia di nuovo
|
||||||
|
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Invio massivo
|
bulk: Invio massivo
|
||||||
@@ -2511,8 +2577,14 @@ it: &it
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from}-%{to} di %{count} elementi"
|
range_with_total: "%{from}-%{to} di %{count} elementi"
|
||||||
range_without_total: "%{from}-%{to} elementi"
|
range_without_total: "%{from}-%{to} elementi"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from}-%{to} di %{count} eventi"
|
||||||
|
range_without_total: "%{from}-%{to} eventi"
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
default_parties: Parties par défaut
|
||||||
|
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
|
||||||
|
require_all_recipients: Exiger tous les destinataires
|
||||||
stripe_integration: Intégration Stripe
|
stripe_integration: Intégration Stripe
|
||||||
stripe_account_has_been_connected: Le compte Stripe a été connecté.
|
stripe_account_has_been_connected: Le compte Stripe a été connecté.
|
||||||
re_connect_stripe: Reconnecter Stripe
|
re_connect_stripe: Reconnecter Stripe
|
||||||
@@ -2607,7 +2679,12 @@ fr: &fr
|
|||||||
digitally_signed_by: Signé numériquement par
|
digitally_signed_by: Signé numériquement par
|
||||||
role: Rôle
|
role: Rôle
|
||||||
reason: Raison
|
reason: Raison
|
||||||
provide_your_email_to_start: Entrez votre adresse email pour commencer
|
provide_your_email_to_start: Fournissez votre e-mail pour commencer
|
||||||
|
provide_your_email: Fournissez votre e-mail
|
||||||
|
provide_your_name_to_start: Fournissez votre nom pour commencer
|
||||||
|
provide_your_name: Fournissez votre nom
|
||||||
|
provide_your_phone_in_international_format_to_start: Fournissez votre téléphone au format international pour commencer
|
||||||
|
provide_your_phone_in_international_format: Fournissez votre téléphone au format international
|
||||||
start: Démarrer
|
start: Démarrer
|
||||||
starting: Démarrage
|
starting: Démarrage
|
||||||
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
|
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
|
||||||
@@ -3244,6 +3321,18 @@ fr: &fr
|
|||||||
share_link: Lien de partage
|
share_link: Lien de partage
|
||||||
enable_shared_link: Activer le lien de partage
|
enable_shared_link: Activer le lien de partage
|
||||||
share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
|
share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
|
||||||
|
select_data_residency: Sélectionner la résidence des données
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} vous a invité à remplir et signer des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Consultez ou téléchargez les documents complétés. Remplissez et signez des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.
|
||||||
|
link_form_fields: Lier les champs du formulaire
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: Au moins un champ doit être affiché dans le formulaire.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Ce modèle contient plusieurs parties, ce qui empêche l’utilisation d’un lien de partage car il n’est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut.
|
||||||
|
events_log: Journal des événements
|
||||||
|
successed: Réussi
|
||||||
|
failed: Échoué
|
||||||
|
there_are_no_events: Aucun événement
|
||||||
|
resend: Renvoyer
|
||||||
|
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envoi en masse
|
bulk: Envoi en masse
|
||||||
@@ -3344,8 +3433,14 @@ fr: &fr
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from} à %{to} sur %{count} éléments"
|
range_with_total: "%{from} à %{to} sur %{count} éléments"
|
||||||
range_without_total: "%{from} à %{to} éléments"
|
range_without_total: "%{from} à %{to} éléments"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from} à %{to} sur %{count} événements"
|
||||||
|
range_without_total: "%{from} à %{to} événements"
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
default_parties: Partes padrão
|
||||||
|
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
|
||||||
|
require_all_recipients: Exigir todos os destinatários
|
||||||
stripe_integration: Integração com Stripe
|
stripe_integration: Integração com Stripe
|
||||||
stripe_account_has_been_connected: Conta Stripe foi conectada.
|
stripe_account_has_been_connected: Conta Stripe foi conectada.
|
||||||
re_connect_stripe: Reconectar Stripe
|
re_connect_stripe: Reconectar Stripe
|
||||||
@@ -3440,7 +3535,12 @@ pt: &pt
|
|||||||
digitally_signed_by: Assinado digitalmente por
|
digitally_signed_by: Assinado digitalmente por
|
||||||
role: Função
|
role: Função
|
||||||
reason: Motivo
|
reason: Motivo
|
||||||
provide_your_email_to_start: Forneça o seu email para começar
|
provide_your_email_to_start: Informe seu e-mail para começar
|
||||||
|
provide_your_email: Informe seu e-mail
|
||||||
|
provide_your_name_to_start: Informe seu nome para começar
|
||||||
|
provide_your_name: Informe seu nome
|
||||||
|
provide_your_phone_in_international_format_to_start: Informe seu telefone no formato internacional para começar
|
||||||
|
provide_your_phone_in_international_format: Informe seu telefone no formato internacional
|
||||||
start: Iniciar
|
start: Iniciar
|
||||||
starting: Iniciando
|
starting: Iniciando
|
||||||
form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.'
|
form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.'
|
||||||
@@ -4076,6 +4176,18 @@ pt: &pt
|
|||||||
share_link: Link de compartilhamento
|
share_link: Link de compartilhamento
|
||||||
enable_shared_link: Ativar link compartilhado
|
enable_shared_link: Ativar link compartilhado
|
||||||
share_link_is_currently_disabled: O link compartilhado está desativado no momento
|
share_link_is_currently_disabled: O link compartilhado está desativado no momento
|
||||||
|
select_data_residency: Selecionar local dos dados
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} convidou você para preencher e assinar documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Reveja ou baixe os documentos concluídos. Preencha e assine documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.
|
||||||
|
link_form_fields: Vincular campos do formulário
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: Pelo menos um campo deve ser exibido no formulário.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Este modelo tem várias partes, o que impede o uso de um link de compartilhamento, pois não está claro qual parte é responsável por campos específicos. Para resolver isso, defina os detalhes padrão da parte.
|
||||||
|
events_log: Registro de eventos
|
||||||
|
successed: Sucesso
|
||||||
|
failed: Falhou
|
||||||
|
there_are_no_events: Nenhum evento
|
||||||
|
resend: Reenviar
|
||||||
|
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envio em massa
|
bulk: Envio em massa
|
||||||
@@ -4177,8 +4289,14 @@ pt: &pt
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from}-%{to} de %{count} itens"
|
range_with_total: "%{from}-%{to} de %{count} itens"
|
||||||
range_without_total: "%{from}-%{to} itens"
|
range_without_total: "%{from}-%{to} itens"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||||
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
default_parties: Standardparteien
|
||||||
|
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
|
||||||
|
require_all_recipients: Alle Empfänger erforderlich
|
||||||
stripe_integration: Stripe-Integration
|
stripe_integration: Stripe-Integration
|
||||||
stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
|
stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
|
||||||
re_connect_stripe: Stripe erneut verbinden
|
re_connect_stripe: Stripe erneut verbinden
|
||||||
@@ -4273,7 +4391,12 @@ de: &de
|
|||||||
digitally_signed_by: Digital signiert von
|
digitally_signed_by: Digital signiert von
|
||||||
role: Rolle
|
role: Rolle
|
||||||
reason: Grund
|
reason: Grund
|
||||||
provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten
|
provide_your_email_to_start: Geben Sie Ihre E-Mail-Adresse ein, um zu starten
|
||||||
|
provide_your_email: Geben Sie Ihre E-Mail-Adresse ein
|
||||||
|
provide_your_name_to_start: Geben Sie Ihren Namen ein, um zu starten
|
||||||
|
provide_your_name: Geben Sie Ihren Namen ein
|
||||||
|
provide_your_phone_in_international_format_to_start: Geben Sie Ihre Telefonnummer im internationalen Format ein, um zu starten
|
||||||
|
provide_your_phone_in_international_format: Geben Sie Ihre Telefonnummer im internationalen Format ein
|
||||||
start: Starten
|
start: Starten
|
||||||
starting: Starten
|
starting: Starten
|
||||||
form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.'
|
form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.'
|
||||||
@@ -4909,6 +5032,18 @@ de: &de
|
|||||||
share_link: Freigabelink
|
share_link: Freigabelink
|
||||||
enable_shared_link: 'Freigabelink aktivieren'
|
enable_shared_link: 'Freigabelink aktivieren'
|
||||||
share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
|
share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
|
||||||
|
select_data_residency: Datenstandort auswählen
|
||||||
|
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} hat Sie eingeladen, Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung auszufüllen und zu unterschreiben.'
|
||||||
|
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Überprüfen oder laden Sie abgeschlossene Dokumente herunter. Füllen und unterschreiben Sie Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung.
|
||||||
|
link_form_fields: Formularfelder verknüpfen
|
||||||
|
at_least_one_field_must_be_displayed_in_the_form: Mindestens ein Feld muss im Formular angezeigt werden.
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Diese Vorlage enthält mehrere Parteien, was die Verwendung eines Freigabelinks verhindert, da unklar ist, welche Partei für bestimmte Felder verantwortlich ist. Um dies zu beheben, definieren Sie die Standardparteidetails.
|
||||||
|
events_log: Ereignisprotokoll
|
||||||
|
successed: Erfolgreich
|
||||||
|
failed: Fehlgeschlagen
|
||||||
|
there_are_no_events: Keine Ereignisse vorhanden
|
||||||
|
resend: Erneut senden
|
||||||
|
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Massenversand
|
bulk: Massenversand
|
||||||
@@ -5009,6 +5144,9 @@ de: &de
|
|||||||
items:
|
items:
|
||||||
range_with_total: "%{from}-%{to} von %{count} Elementen"
|
range_with_total: "%{from}-%{to} von %{count} Elementen"
|
||||||
range_without_total: "%{from}-%{to} Elemente"
|
range_without_total: "%{from}-%{to} Elemente"
|
||||||
|
events:
|
||||||
|
range_with_total: "%{from}-%{to} von %{count} Ereignissen"
|
||||||
|
range_without_total: "%{from}-%{to} Ereignisse"
|
||||||
|
|
||||||
pl:
|
pl:
|
||||||
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
|
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
|
||||||
@@ -5033,7 +5171,13 @@ pl:
|
|||||||
digitally_signed_by: Podpis cyfrowy przez
|
digitally_signed_by: Podpis cyfrowy przez
|
||||||
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>'
|
||||||
role: Rola
|
role: Rola
|
||||||
provide_your_email_to_start: Podaj swój adres email, aby rozpocząć
|
provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
|
||||||
|
provide_your_email: Podaj swój e-mail
|
||||||
|
provide_your_name_to_start: Podaj swoje imię, aby rozpocząć
|
||||||
|
provide_your_name: Podaj swoje imię
|
||||||
|
provide_your_phone_in_international_format_to_start: Podaj numer telefonu w formacie międzynarodowym, aby rozpocząć
|
||||||
|
provide_your_phone_in_international_format: Podaj numer telefonu w formacie międzynarodowym
|
||||||
|
use_international_format_1xxx_: 'Użyj formatu międzynarodowego: +1xxx...'
|
||||||
start: Rozpocznij
|
start: Rozpocznij
|
||||||
reason: Powód
|
reason: Powód
|
||||||
starting: Rozpoczynanie
|
starting: Rozpoczynanie
|
||||||
@@ -5080,6 +5224,9 @@ pl:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
|
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
|
||||||
privacy_policy: Polityka Prywatności
|
privacy_policy: Polityka Prywatności
|
||||||
esignature_disclosure: Użycie e-podpisu
|
esignature_disclosure: Użycie e-podpisu
|
||||||
|
select_data_residency: Wybierz lokalizację danych
|
||||||
|
company_name: Nazwa firmy
|
||||||
|
optional: opcjonalne
|
||||||
|
|
||||||
uk:
|
uk:
|
||||||
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
||||||
@@ -5103,7 +5250,13 @@ uk:
|
|||||||
digitally_signed_by: Цифровий підпис від
|
digitally_signed_by: Цифровий підпис від
|
||||||
verification_code_code: 'Код підтвердження: %{code}'
|
verification_code_code: 'Код підтвердження: %{code}'
|
||||||
role: Роль
|
role: Роль
|
||||||
provide_your_email_to_start: Введіть свій email, щоб почати
|
provide_your_email_to_start: Введіть свій емейл, щоб почати
|
||||||
|
provide_your_email: Введіть свій емейл
|
||||||
|
provide_your_name_to_start: Введіть своє ім’я, щоб почати
|
||||||
|
provide_your_name: Введіть своє ім’я
|
||||||
|
provide_your_phone_in_international_format_to_start: Введіть номер телефону у міжнародному форматі, щоб почати
|
||||||
|
provide_your_phone_in_international_format: Введіть номер телефону у міжнародному форматі
|
||||||
|
use_international_format_1xxx_: 'Використовуйте міжнародний формат: +1xxx...'
|
||||||
form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>'
|
||||||
start: Почати
|
start: Почати
|
||||||
reason: Причина
|
reason: Причина
|
||||||
@@ -5151,6 +5304,9 @@ uk:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
|
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
|
||||||
privacy_policy: Політика конфіденційності
|
privacy_policy: Політика конфіденційності
|
||||||
esignature_disclosure: Використання e-підпису
|
esignature_disclosure: Використання e-підпису
|
||||||
|
select_data_residency: Виберіть місце зберігання даних
|
||||||
|
company_name: Назва компанії
|
||||||
|
optional: необов’язково
|
||||||
|
|
||||||
cs:
|
cs:
|
||||||
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
||||||
@@ -5174,7 +5330,13 @@ cs:
|
|||||||
digitally_signed_by: Digitálně podepsáno uživatelem
|
digitally_signed_by: Digitálně podepsáno uživatelem
|
||||||
verification_code_code: 'Ověřovací kód: %{code}'
|
verification_code_code: 'Ověřovací kód: %{code}'
|
||||||
role: Role
|
role: Role
|
||||||
provide_your_email_to_start: Zadejte svůj email pro zahájení
|
provide_your_email_to_start: Zadejte svůj e-mail pro zahájení
|
||||||
|
provide_your_email: Zadejte svůj e-mail
|
||||||
|
provide_your_name_to_start: Zadejte své jméno pro zahájení
|
||||||
|
provide_your_name: Zadejte své jméno
|
||||||
|
provide_your_phone_in_international_format_to_start: Zadejte své telefonní číslo v mezinárodním formátu pro zahájení
|
||||||
|
provide_your_phone_in_international_format: Zadejte své telefonní číslo v mezinárodním formátu
|
||||||
|
use_international_format_1xxx_: 'Použijte mezinárodní formát: +1xxx...'
|
||||||
reason: Důvod
|
reason: Důvod
|
||||||
form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>'
|
||||||
start: Zahájit
|
start: Zahájit
|
||||||
@@ -5222,6 +5384,9 @@ cs:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
|
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
|
||||||
privacy_policy: Zásady Ochrany Osobních Údajů
|
privacy_policy: Zásady Ochrany Osobních Údajů
|
||||||
esignature_disclosure: Použití e-podpisu
|
esignature_disclosure: Použití e-podpisu
|
||||||
|
select_data_residency: Vyberte umístění dat
|
||||||
|
company_name: Název společnosti
|
||||||
|
optional: volitelné
|
||||||
|
|
||||||
he:
|
he:
|
||||||
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
||||||
@@ -5246,7 +5411,13 @@ he:
|
|||||||
role: תפקיד
|
role: תפקיד
|
||||||
reason: סיבה
|
reason: סיבה
|
||||||
verification_code_code: 'קוד אימות: %{code}'
|
verification_code_code: 'קוד אימות: %{code}'
|
||||||
provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל
|
provide_your_email_to_start: הזן את האימייל שלך כדי להתחיל
|
||||||
|
provide_your_email: הזן את האימייל שלך
|
||||||
|
provide_your_name_to_start: הזן את שמך כדי להתחיל
|
||||||
|
provide_your_name: הזן את שמך
|
||||||
|
provide_your_phone_in_international_format_to_start: הזן את מספר הטלפון שלך בפורמט בינלאומי כדי להתחיל
|
||||||
|
provide_your_phone_in_international_format: הזן את מספר הטלפון שלך בפורמט בינלאומי
|
||||||
|
use_international_format_1xxx_: 'השתמש בפורמט בינלאומי: +1xxx...'
|
||||||
start: התחל
|
start: התחל
|
||||||
starting: מתחיל
|
starting: מתחיל
|
||||||
form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>'
|
||||||
@@ -5293,6 +5464,9 @@ he:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
|
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
|
||||||
privacy_policy: 'מדיניות פרטיות'
|
privacy_policy: 'מדיניות פרטיות'
|
||||||
esignature_disclosure: 'גילוי חתימה אלקטרונית'
|
esignature_disclosure: 'גילוי חתימה אלקטרונית'
|
||||||
|
select_data_residency: בחר מיקום נתונים
|
||||||
|
company_name: שם החברה
|
||||||
|
optional: אופציונלי
|
||||||
|
|
||||||
nl:
|
nl:
|
||||||
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
||||||
@@ -5316,7 +5490,13 @@ nl:
|
|||||||
digitally_signed_by: Digitaal ondertekend door
|
digitally_signed_by: Digitaal ondertekend door
|
||||||
role: Rol
|
role: Rol
|
||||||
verification_code_code: 'Verificatiecode: %{code}'
|
verification_code_code: 'Verificatiecode: %{code}'
|
||||||
provide_your_email_to_start: Geef uw e-mailadres om te beginnen
|
provide_your_email_to_start: Voer je e-mailadres in om te beginnen
|
||||||
|
provide_your_email: Voer je e-mailadres in
|
||||||
|
provide_your_name_to_start: Voer je naam in om te beginnen
|
||||||
|
provide_your_name: Voer je naam in
|
||||||
|
provide_your_phone_in_international_format_to_start: Voer je telefoonnummer in internationaal formaat in om te beginnen
|
||||||
|
provide_your_phone_in_international_format: Voer je telefoonnummer in internationaal formaat in
|
||||||
|
use_international_format_1xxx_: 'Gebruik het internationale formaat: +1xxx...'
|
||||||
start: Start
|
start: Start
|
||||||
reason: Reden
|
reason: Reden
|
||||||
form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>'
|
||||||
@@ -5364,6 +5544,9 @@ nl:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
|
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
|
||||||
privacy_policy: Privacybeleid
|
privacy_policy: Privacybeleid
|
||||||
esignature_disclosure: Gebruik van e-handtekening
|
esignature_disclosure: Gebruik van e-handtekening
|
||||||
|
select_data_residency: Selecteer gegevenslocatie
|
||||||
|
company_name: Bedrijfsnaam
|
||||||
|
optional: optioneel
|
||||||
|
|
||||||
ar:
|
ar:
|
||||||
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
||||||
@@ -5386,7 +5569,13 @@ ar:
|
|||||||
email: البريد الإلكتروني
|
email: البريد الإلكتروني
|
||||||
digitally_signed_by: تم التوقيع الرقمي بواسطة
|
digitally_signed_by: تم التوقيع الرقمي بواسطة
|
||||||
role: الدور
|
role: الدور
|
||||||
provide_your_email_to_start: قدم بريدك الإلكتروني للبدء
|
provide_your_email_to_start: أدخل بريدك الإلكتروني للبدء
|
||||||
|
provide_your_email: أدخل بريدك الإلكتروني
|
||||||
|
provide_your_name_to_start: أدخل اسمك للبدء
|
||||||
|
provide_your_name: أدخل اسمك
|
||||||
|
provide_your_phone_in_international_format_to_start: أدخل رقم هاتفك بصيغة دولية للبدء
|
||||||
|
provide_your_phone_in_international_format: أدخل رقم هاتفك بصيغة دولية
|
||||||
|
use_international_format_1xxx_: 'استخدم التنسيق الدولي: +1xxx...'
|
||||||
start: بدء
|
start: بدء
|
||||||
starting: بداية
|
starting: بداية
|
||||||
verification_code_code: 'رمز التحقق: %{code}'
|
verification_code_code: 'رمز التحقق: %{code}'
|
||||||
@@ -5435,6 +5624,9 @@ ar:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
|
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
|
||||||
privacy_policy: 'سياسة الخصوصية'
|
privacy_policy: 'سياسة الخصوصية'
|
||||||
esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
|
esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
|
||||||
|
select_data_residency: اختر موقع تخزين البيانات
|
||||||
|
company_name: اسم الشركة
|
||||||
|
optional: اختياري
|
||||||
|
|
||||||
ko:
|
ko:
|
||||||
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
||||||
@@ -5457,7 +5649,13 @@ ko:
|
|||||||
email: 이메일
|
email: 이메일
|
||||||
digitally_signed_by: 디지털로 서명됨
|
digitally_signed_by: 디지털로 서명됨
|
||||||
role: 역할
|
role: 역할
|
||||||
provide_your_email_to_start: 시작하려면 이메일을 제공하세요
|
provide_your_email_to_start: 시작하려면 이메일을 입력하세요
|
||||||
|
provide_your_email: 이메일을 입력하세요
|
||||||
|
provide_your_name_to_start: 시작하려면 이름을 입력하세요
|
||||||
|
provide_your_name: 이름을 입력하세요
|
||||||
|
provide_your_phone_in_international_format_to_start: 시작하려면 국제 형식의 전화번호를 입력하세요
|
||||||
|
provide_your_phone_in_international_format: 국제 형식의 전화번호를 입력하세요
|
||||||
|
use_international_format_1xxx_: '국제 형식을 사용하세요: +1xxx...'
|
||||||
start: 시작
|
start: 시작
|
||||||
reason: 이유
|
reason: 이유
|
||||||
starting: 시작 중
|
starting: 시작 중
|
||||||
@@ -5506,6 +5704,9 @@ ko:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
|
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
|
||||||
privacy_policy: 개인정보 처리방침
|
privacy_policy: 개인정보 처리방침
|
||||||
esignature_disclosure: 전자서명 공개
|
esignature_disclosure: 전자서명 공개
|
||||||
|
select_data_residency: 데이터 저장 위치 선택
|
||||||
|
company_name: 회사 이름
|
||||||
|
optional: 선택 사항
|
||||||
|
|
||||||
ja:
|
ja:
|
||||||
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
||||||
@@ -5529,6 +5730,12 @@ ja:
|
|||||||
digitally_signed_by: 電子署名者
|
digitally_signed_by: 電子署名者
|
||||||
role: 役割
|
role: 役割
|
||||||
provide_your_email_to_start: 開始するにはメールアドレスを入力してください
|
provide_your_email_to_start: 開始するにはメールアドレスを入力してください
|
||||||
|
provide_your_email: メールアドレスを入力してください
|
||||||
|
provide_your_name_to_start: 開始するには名前を入力してください
|
||||||
|
provide_your_name: 名前を入力してください
|
||||||
|
provide_your_phone_in_international_format_to_start: 開始するには国際形式の電話番号を入力してください
|
||||||
|
provide_your_phone_in_international_format: 国際形式の電話番号を入力してください
|
||||||
|
use_international_format_1xxx_: '国際形式を使用してください:+1xxx...'
|
||||||
start: 開始
|
start: 開始
|
||||||
reason: 理由
|
reason: 理由
|
||||||
starting: 開始中
|
starting: 開始中
|
||||||
@@ -5577,6 +5784,9 @@ ja:
|
|||||||
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
|
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
|
||||||
privacy_policy: プライバシーポリシー
|
privacy_policy: プライバシーポリシー
|
||||||
esignature_disclosure: 電子署名に関する開示
|
esignature_disclosure: 電子署名に関する開示
|
||||||
|
select_data_residency: データ保存場所を選択
|
||||||
|
company_name: 会社名
|
||||||
|
optional: 任意
|
||||||
|
|
||||||
en-US:
|
en-US:
|
||||||
<<: *en
|
<<: *en
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ Rails.application.routes.draw do
|
|||||||
scope '/settings', as: :settings do
|
scope '/settings', as: :settings do
|
||||||
unless Docuseal.multitenant?
|
unless Docuseal.multitenant?
|
||||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||||
|
resources :search_entries_reindex, only: %i[create]
|
||||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||||
end
|
end
|
||||||
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
||||||
@@ -178,6 +179,11 @@ Rails.application.routes.draw do
|
|||||||
resources :api, only: %i[index create], controller: 'api_settings'
|
resources :api, only: %i[index create], controller: 'api_settings'
|
||||||
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||||
post :resend
|
post :resend
|
||||||
|
|
||||||
|
resources :events, only: %i[show], controller: 'webhook_events' do
|
||||||
|
post :resend, on: :member
|
||||||
|
post :refresh, on: :member
|
||||||
|
end
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddEmailEventsEventTypeIndex < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_index :email_events, :email, where: "event_type IN ('bounce', 'soft_bounce', 'complaint', 'soft_complaint')",
|
||||||
|
name: 'index_email_events_on_email_event_types'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddTemplatesFolderIndex < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_index :templates, %i[account_id folder_id id], where: 'archived_at IS NULL'
|
||||||
|
end
|
||||||
|
end
|
||||||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user