Merge from docusealco/wip

pull/555/merge 2.4.1
Alex Turchyn 1 month ago committed by GitHub
commit 22d4c1bd10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,6 +4,7 @@ source 'https://rubygems.org'
ruby '4.0.1' ruby '4.0.1'
gem 'addressable'
gem 'arabic-letter-connector', require: false gem 'arabic-letter-connector', require: false
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'aws-sdk-secretsmanager', require: false gem 'aws-sdk-secretsmanager', require: false
@ -28,7 +29,6 @@ gem 'oj'
gem 'onnxruntime', require: false gem 'onnxruntime', require: false
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender' gem 'pretender'
gem 'puma', require: false gem 'puma', require: false
gem 'rack' gem 'rack'
@ -43,7 +43,7 @@ gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false gem 'trilogy', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'

@ -1,11 +1,3 @@
GIT
remote: https://github.com/trilogy-libraries/trilogy.git
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
glob: contrib/ruby/*.gemspec
specs:
trilogy (2.10.0)
bigdecimal
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -116,7 +108,7 @@ GEM
cgi cgi
rexml rexml
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.21) bcrypt (3.1.22)
better_html (2.2.0) better_html (2.2.0)
actionview (>= 7.0) actionview (>= 7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
@ -158,8 +150,6 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.21.1)
addressable
csv (3.3.5) csv (3.3.5)
csv-safe (3.3.1) csv-safe (3.3.1)
csv (~> 3.0) csv (~> 3.0)
@ -171,16 +161,16 @@ GEM
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
devise (4.9.4) devise (5.0.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 7.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.3.1) devise-two-factor (6.4.0)
activesupport (>= 7.0, < 8.2) activesupport (>= 7.2, < 8.2)
devise (>= 4.0, < 5.0) devise (>= 4.0, < 6.0)
railties (>= 7.0, < 8.2) railties (>= 7.2, < 8.2)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.6.2) diff-lcs (1.6.2)
digest-crc (0.7.0) digest-crc (0.7.0)
@ -189,7 +179,7 @@ GEM
dotenv (3.2.0) dotenv (3.2.0)
drb (2.2.3) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (6.0.1) erb (6.0.2)
erb_lint (0.9.0) erb_lint (0.9.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@ -272,19 +262,19 @@ GEM
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
strscan (>= 3.1.2) strscan (>= 3.1.2)
htmlentities (4.4.2)
i18n (1.14.8) i18n (1.14.8)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.14.0) image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6) mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2) io-console (0.8.2)
irb (1.16.0) irb (1.17.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.18.1) json (2.19.2)
jwt (3.1.2) jwt (3.1.2)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
@ -306,7 +296,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.25.0) loofah (2.25.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.9.0) mail (2.9.0)
@ -321,7 +311,8 @@ GEM
mini_magick (5.3.1) mini_magick (5.3.1)
logger logger
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (6.0.1) minitest (6.0.2)
drb (~> 2.0)
prism (~> 1.5) prism (~> 1.5)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.19.1) multi_json (1.19.1)
@ -337,17 +328,17 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.1-aarch64-linux-gnu) nokogiri (1.19.2-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-aarch64-linux-musl) nokogiri (1.19.2-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm64-darwin) nokogiri (1.19.2-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu) nokogiri (1.19.2-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl) nokogiri (1.19.2-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
numo-narray-alt (0.9.13) numo-narray-alt (0.10.3)
oj (3.16.13) oj (3.16.13)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
@ -376,18 +367,10 @@ GEM
pg (1.6.3-x86_64-linux-musl) pg (1.6.3-x86_64-linux-musl)
pp (0.6.3) pp (0.6.3)
prettyprint prettyprint
premailer (1.27.0)
addressable
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
premailer-rails (1.12.0)
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
pretender (0.6.0) pretender (0.6.0)
actionpack (>= 7.1) actionpack (>= 7.1)
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.8.0) prism (1.9.0)
pry (0.16.0) pry (0.16.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
@ -429,8 +412,8 @@ GEM
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.7.0)
loofah (~> 2.21) loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.1.0) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
@ -446,7 +429,7 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.1) rake (13.3.1)
rdoc (7.1.0) rdoc (7.2.0)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort tsort
@ -562,6 +545,8 @@ GEM
thor (1.5.0) thor (1.5.0)
timeout (0.6.0) timeout (0.6.0)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
trilogy (2.10.0)
bigdecimal
tsort (0.2.0) tsort (0.2.0)
turbo-rails (2.0.21) turbo-rails (2.0.21)
actionpack (>= 7.1.0) actionpack (>= 7.1.0)
@ -601,7 +586,7 @@ GEM
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml (0.4.0) yaml (0.4.0)
zeitwerk (2.7.4) zeitwerk (2.7.5)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@ -611,6 +596,7 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
addressable
annotaterb annotaterb
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
@ -647,7 +633,6 @@ DEPENDENCIES
onnxruntime onnxruntime
pagy pagy
pg pg
premailer-rails
pretender pretender
pry-rails pry-rails
puma puma
@ -669,7 +654,7 @@ DEPENDENCIES
simplecov simplecov
sqlite3 sqlite3
strip_attributes strip_attributes
trilogy! trilogy
turbo-rails turbo-rails
twitter_cldr twitter_cldr
tzinfo-data tzinfo-data

@ -69,6 +69,7 @@ module Api
SearchEntries.enqueue_reindex(@template) SearchEntries.enqueue_reindex(@template)
WebhookUrls.enqueue_events(@template, 'template.updated') WebhookUrls.enqueue_events(@template, 'template.updated')
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
@ -78,6 +79,8 @@ module Api
@template.destroy! @template.destroy!
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@template, 'template.archived')
end end
render json: @template.as_json(only: %i[id archived_at]) render json: @template.as_json(only: %i[id archived_at])

@ -84,6 +84,8 @@ class TemplatesController < ApplicationController
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@template, 'template.archived')
I18n.t('template_has_been_archived') I18n.t('template_has_been_archived')
end end

@ -6,6 +6,8 @@ class TemplatesRestoreController < ApplicationController
def create def create
@template.update!(archived_at: nil) @template.update!(archived_at: nil)
WebhookUrls.enqueue_events(@template, 'template.updated')
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived') redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
end end
end end

@ -15,7 +15,7 @@ class WebhookEventsController < ApplicationController
Submissions::SerializeForApi.call(@webhook_event.record) Submissions::SerializeForApi.call(@webhook_event.record)
when 'template.created', 'template.updated' when 'template.created', 'template.updated'
Templates::SerializeForApi.call(@webhook_event.record) Templates::SerializeForApi.call(@webhook_event.record)
when 'submission.archived' when 'submission.archived', 'template.archived'
@webhook_event.record.as_json(only: %i[id archived_at]) @webhook_event.record.as_json(only: %i[id archived_at])
end end
end end

@ -2735,10 +2735,13 @@ export default {
} else { } else {
this.isSaving = true this.isSaving = true
this.documentRefs.filter((ref) => ref.update).map((ref) => ref.update()) const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.map((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false }) this.rebuildVariablesSchema({ disable: false })
const dynamicDocumentSaves = this.documentRefs.filter((ref) => ref.saveBody).map((ref) => ref.saveBody()) const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => { Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => {
window.Turbo.visit(`/templates/${this.template.id}`) window.Turbo.visit(`/templates/${this.template.id}`)
@ -3028,6 +3031,8 @@ export default {
} else { } else {
dynamicDocumentRef.syncVariablesSchema(this.template.variables_schema, parsed, { disable }) dynamicDocumentRef.syncVariablesSchema(this.template.variables_schema, parsed, { disable })
} }
} else {
this.template.variables_schema = {}
} }
} }
} }

@ -0,0 +1,38 @@
# frozen_string_literal: true
class SendTemplateArchivedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find_by(id: params['template_id'])
return unless template
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.archived')
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.archived',
event_uuid: params['event_uuid'],
record: template,
attempt:,
data: template.as_json(only: %i[id archived_at]))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end

@ -5,6 +5,8 @@ class ApplicationMailer < ActionMailer::Base
layout 'mailer' layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor
register_interceptor HtmlToPlainTextInterceptor
register_preview_interceptor HtmlToPlainTextInterceptor
register_observer ActionMailerEventsObserver register_observer ActionMailerEventsObserver

@ -34,6 +34,7 @@ class WebhookUrl < ApplicationRecord
submission.archived submission.archived
template.created template.created
template.updated template.updated
template.archived
].freeze ].freeze
belongs_to :account belongs_to :account

@ -88,6 +88,8 @@ Rails.application.configure do
openssl_verify_mode: ENV['SMTP_SSL_VERIFY'] == 'false' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER, openssl_verify_mode: ENV['SMTP_SSL_VERIFY'] == 'false' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil, authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false', enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false',
ssl: ENV['SMTP_ENABLE_SSL'] == 'true',
tls: ENV['SMTP_ENABLE_TLS'] == 'true',
open_timeout: ENV.fetch('SMTP_OPEN_TIMEOUT', '15').to_i, open_timeout: ENV.fetch('SMTP_OPEN_TIMEOUT', '15').to_i,
read_timeout: ENV.fetch('SMTP_READ_TIMEOUT', '25').to_i read_timeout: ENV.fetch('SMTP_READ_TIMEOUT', '25').to_i
}.compact }.compact

@ -914,6 +914,9 @@ en: &en
connect_to_docuseal_mcp: Connect to DocuSeal MCP connect_to_docuseal_mcp: Connect to DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Add the following to your MCP client configuration add_the_following_to_your_mcp_client_configuration: Add the following to your MCP client configuration
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Works with Claude Desktop, Cursor, Windsurf, VS Code, and any MCP-compatible client. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Works with Claude Desktop, Cursor, Windsurf, VS Code, and any MCP-compatible client.
your_email_address_has_been_changed: Your email address has been changed
the_email_address_for_your_account_has_been_changed_to_new_email: The email address for your account has been changed to %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: If you did not make this change, please contact us by replying to this email.
devise: devise:
confirmations: confirmations:
confirmed: Your email address has been successfully confirmed. confirmed: Your email address has been successfully confirmed.
@ -1938,6 +1941,9 @@ es: &es
connect_to_docuseal_mcp: Conectar a DocuSeal MCP connect_to_docuseal_mcp: Conectar a DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Agregue lo siguiente a la configuración de su cliente MCP add_the_following_to_your_mcp_client_configuration: Agregue lo siguiente a la configuración de su cliente MCP
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona con Claude Desktop, Cursor, Windsurf, VS Code y cualquier cliente compatible con MCP. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona con Claude Desktop, Cursor, Windsurf, VS Code y cualquier cliente compatible con MCP.
your_email_address_has_been_changed: Tu dirección de correo electrónico ha sido cambiada
the_email_address_for_your_account_has_been_changed_to_new_email: La dirección de correo electrónico de tu cuenta ha sido cambiada a %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si no realizaste este cambio, contáctanos respondiendo a este correo electrónico.
devise: devise:
confirmations: confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente. confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@ -2963,6 +2969,9 @@ it: &it
connect_to_docuseal_mcp: Connetti a DocuSeal MCP connect_to_docuseal_mcp: Connetti a DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Aggiungi quanto segue alla configurazione del tuo client MCP add_the_following_to_your_mcp_client_configuration: Aggiungi quanto segue alla configurazione del tuo client MCP
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funziona con Claude Desktop, Cursor, Windsurf, VS Code e qualsiasi client compatibile con MCP. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funziona con Claude Desktop, Cursor, Windsurf, VS Code e qualsiasi client compatibile con MCP.
your_email_address_has_been_changed: Il tuo indirizzo email è stato modificato
the_email_address_for_your_account_has_been_changed_to_new_email: L'indirizzo email del tuo account è stato modificato in %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se non hai effettuato questa modifica, contattaci rispondendo a questa email.
devise: devise:
confirmations: confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo. confirmed: Il tuo indirizzo email è stato confermato con successo.
@ -3984,6 +3993,9 @@ fr: &fr
connect_to_docuseal_mcp: Se connecter à DocuSeal MCP connect_to_docuseal_mcp: Se connecter à DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Ajoutez ce qui suit à la configuration de votre client MCP add_the_following_to_your_mcp_client_configuration: Ajoutez ce qui suit à la configuration de votre client MCP
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Fonctionne avec Claude Desktop, Cursor, Windsurf, VS Code et tout client compatible MCP. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Fonctionne avec Claude Desktop, Cursor, Windsurf, VS Code et tout client compatible MCP.
your_email_address_has_been_changed: Votre adresse e-mail a été modifiée
the_email_address_for_your_account_has_been_changed_to_new_email: "L'adresse e-mail de votre compte a été modifiée en %{new_email}."
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si vous n'avez pas effectué ce changement, veuillez nous contacter en répondant à cet e-mail.
devise: devise:
confirmations: confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès. confirmed: Votre adresse e-mail a été confirmée avec succès.
@ -5008,6 +5020,9 @@ pt: &pt
connect_to_docuseal_mcp: Conectar ao DocuSeal MCP connect_to_docuseal_mcp: Conectar ao DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Adicione o seguinte à configuração do seu cliente MCP add_the_following_to_your_mcp_client_configuration: Adicione o seguinte à configuração do seu cliente MCP
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona com Claude Desktop, Cursor, Windsurf, VS Code e qualquer cliente compatível com MCP. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona com Claude Desktop, Cursor, Windsurf, VS Code e qualquer cliente compatível com MCP.
your_email_address_has_been_changed: Seu endereço de e-mail foi alterado
the_email_address_for_your_account_has_been_changed_to_new_email: O endereço de e-mail da sua conta foi alterado para %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se você não fez essa alteração, entre em contato conosco respondendo a este e-mail.
devise: devise:
confirmations: confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso. confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@ -6032,6 +6047,9 @@ de: &de
connect_to_docuseal_mcp: Mit DocuSeal MCP verbinden connect_to_docuseal_mcp: Mit DocuSeal MCP verbinden
add_the_following_to_your_mcp_client_configuration: Fügen Sie Folgendes zu Ihrer MCP-Client-Konfiguration hinzu add_the_following_to_your_mcp_client_configuration: Fügen Sie Folgendes zu Ihrer MCP-Client-Konfiguration hinzu
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funktioniert mit Claude Desktop, Cursor, Windsurf, VS Code und jedem MCP-kompatiblen Client. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funktioniert mit Claude Desktop, Cursor, Windsurf, VS Code und jedem MCP-kompatiblen Client.
your_email_address_has_been_changed: Ihre E-Mail-Adresse wurde geändert
the_email_address_for_your_account_has_been_changed_to_new_email: Die E-Mail-Adresse Ihres Kontos wurde in %{new_email} geändert.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Wenn Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns bitte, indem Sie auf diese E-Mail antworten.
devise: devise:
confirmations: confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt. confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@ -7441,6 +7459,9 @@ nl: &nl
connect_to_docuseal_mcp: Verbinden met DocuSeal MCP connect_to_docuseal_mcp: Verbinden met DocuSeal MCP
add_the_following_to_your_mcp_client_configuration: Voeg het volgende toe aan uw MCP-clientconfiguratie add_the_following_to_your_mcp_client_configuration: Voeg het volgende toe aan uw MCP-clientconfiguratie
works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Werkt met Claude Desktop, Cursor, Windsurf, VS Code en elke MCP-compatibele client. works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Werkt met Claude Desktop, Cursor, Windsurf, VS Code en elke MCP-compatibele client.
your_email_address_has_been_changed: Je e-mailadres is gewijzigd
the_email_address_for_your_account_has_been_changed_to_new_email: Het e-mailadres van je account is gewijzigd naar %{new_email}.
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Als je deze wijziging niet hebt aangebracht, neem dan contact met ons op door op deze e-mail te antwoorden.
devise: devise:
confirmations: confirmations:
confirmed: Je e-mailadres is succesvol bevestigd. confirmed: Je e-mailadres is succesvol bevestigd.

@ -0,0 +1,119 @@
# frozen_string_literal: true
module HtmlToPlainText
module_function
def call(html, line_length = 65)
return '' if html.nil? || html.strip.empty?
cleaned = html.gsub(%r{<!-- start text/html -->.*?<!-- end text/html -->}m, '')
doc = Nokogiri::HTML.fragment(cleaned)
doc.css('script').each(&:remove)
result = process_nodes(doc, line_length)
result.gsub!(/\r\n?/, "\n")
result.gsub!(/[ \t]*\u00A0+[ \t]*/, ' ')
result.gsub!(/\n[ \t]+/, "\n")
result.gsub!(/[ \t]+\n/, "\n")
result.gsub!(/\n{3,}/, "\n\n")
result = word_wrap(result, line_length)
result.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do
"#{"\n" if ::Regexp.last_match(1) == "\n"}( #{::Regexp.last_match(2)} )#{"\n" if ::Regexp.last_match(3) == "\n"}"
end
result.strip
end
def process_nodes(node, line_length)
result = +''
node.children.each do |child|
case child
when Nokogiri::XML::Text
result << child.text
when Nokogiri::XML::Comment
next
when Nokogiri::XML::Element
result << process_element(child, line_length)
end
end
result
end
def process_element(node, line_length)
case node.name
when 'br'
"\n"
when 'p', 'div'
inner = process_nodes(node, line_length)
inner.strip.empty? ? '' : "#{inner}\n\n"
when 'img'
node['alt'] || ''
when 'a'
process_link(node, line_length)
when /\Ah([1-6])\z/
process_heading(node, ::Regexp.last_match(1).to_i, line_length)
when 'li'
inner = process_nodes(node, line_length)
"* #{inner.strip}\n"
else
process_nodes(node, line_length)
end
end
def process_link(node, line_length)
text = process_nodes(node, line_length).strip
return '' if text.empty?
href = node['href']
href = href.sub(/\Amailto:/i, '') if href
if href.nil? || text.casecmp(href.strip) == 0
text
else
"#{text} ( #{href.strip} )"
end
end
def process_heading(node, level, line_length)
text = +''
node.children.each do |child|
text << if child.name == 'br'
"\n"
else
child.text
end
end
text.strip!
hlength = text.each_line.map { |l| l.strip.length }.max || 0
hlength = line_length if hlength > line_length
decorated = case level
when 1
"#{'*' * hlength}\n#{text}\n#{'*' * hlength}"
when 2
"#{'-' * hlength}\n#{text}\n#{'-' * hlength}"
else
"#{text}\n#{'-' * hlength}"
end
"\n\n#{decorated}\n\n"
end
def word_wrap(txt, line_length)
txt.split("\n").map do |line|
if line.length > line_length
line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip
else
line
end
end.join("\n")
end
end

@ -0,0 +1,65 @@
# frozen_string_literal: true
module HtmlToPlainTextInterceptor
module_function
def delivering_email(message)
process(message)
end
def previewing_email(message)
process(message)
end
def process(message)
return message unless html_part(message)
return message if message.text_part
add_text_part(message)
message
end
def add_text_part(message)
html = html_part(message).decoded
text = HtmlToPlainText.call(html)
text_part = Mail::Part.new do
content_type 'text/plain; charset=UTF-8'
body text
end
if pure_html_message?(message)
message.body = nil
message.content_type = 'multipart/alternative'
message.add_part(text_part)
message.add_part(Mail::Part.new do
content_type 'text/html; charset=UTF-8'
body html
end)
else
alternative = Mail::Part.new(content_type: 'multipart/alternative')
alternative.add_part(text_part)
alternative.add_part(message.html_part)
replace_part(message.parts, message.html_part, alternative)
end
end
def pure_html_message?(message)
message.content_type.to_s.include?('text/html')
end
def html_part(message)
pure_html_message?(message) ? message : message.html_part
end
def replace_part(parts, old_part, new_part)
if (index = parts.index(old_part))
parts[index] = new_part
else
parts.each do |part|
replace_part(part.parts, old_part, new_part) if part.respond_to?(:parts)
end
end
end
end

@ -236,9 +236,10 @@ module Submissions
page[:Annots] ||= [] page[:Annots] ||= []
page[:Annots] = page[:Annots].try(:reject) do |e| page[:Annots] = page[:Annots].try(:reject) do |e|
next if e.is_a?(Integer) || e.is_a?(Symbol) next if e.is_a?(Integer) || e.is_a?(Symbol) || e.is_a?(HexaPDF::PDFArray)
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field') e.present? && e[:A] && !e[:A].is_a?(HexaPDF::PDFArray) &&
e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
end || page[:Annots] end || page[:Annots]
width = page.box.width width = page.box.width

@ -10,7 +10,7 @@ module Templates
pdf.pages.flat_map.with_index do |page, index| pdf.pages.flat_map.with_index do |page, index|
(page[:Annots] || []).filter_map do |annot| (page[:Annots] || []).filter_map do |annot|
next if annot.blank? next if annot.blank?
next if annot.is_a?(Integer) || annot.is_a?(Symbol) next if annot.is_a?(Integer) || annot.is_a?(Symbol) || annot.is_a?(HexaPDF::PDFArray)
next if annot[:A].blank? || annot[:A][:URI].blank? next if annot[:A].blank? || annot[:A][:URI].blank?
next unless annot[:Subtype] == :Link next unless annot[:Subtype] == :Link
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://') next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')

@ -11,7 +11,8 @@ module WebhookUrls
'submission.expired' => SendSubmissionExpiredWebhookRequestJob, 'submission.expired' => SendSubmissionExpiredWebhookRequestJob,
'submission.archived' => SendSubmissionArchivedWebhookRequestJob, 'submission.archived' => SendSubmissionArchivedWebhookRequestJob,
'template.created' => SendTemplateCreatedWebhookRequestJob, 'template.created' => SendTemplateCreatedWebhookRequestJob,
'template.updated' => SendTemplateUpdatedWebhookRequestJob 'template.updated' => SendTemplateUpdatedWebhookRequestJob,
'template.archived' => SendTemplateArchivedWebhookRequestJob
}.freeze }.freeze
EVENT_TYPE_ID_KEYS = { EVENT_TYPE_ID_KEYS = {

@ -0,0 +1,100 @@
# frozen_string_literal: true
RSpec.describe SendTemplateArchivedWebhookRequestJob do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
let(:template) { create(:template, account:, author: user) }
let(:webhook_url) { create(:webhook_url, account:, events: ['template.archived']) }
before do
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem))
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end
it 'sends a webhook request' do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
'event_uuid' => SecureRandom.uuid)
expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: {
'event_type' => 'template.archived',
'timestamp' => /.*/,
'data' => template.reload.as_json(only: %i[id archived_at])
},
headers: {
'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook'
}
).once
end
it 'sends a webhook request with the secret' do
webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
'event_uuid' => SecureRandom.uuid)
expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: {
'event_type' => 'template.archived',
'timestamp' => /.*/,
'data' => template.reload.as_json(only: %i[id archived_at])
},
headers: {
'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook',
'X-Secret-Header' => 'secret_value'
}
).once
end
it "doesn't send a webhook request if the event is not in the webhook's events" do
webhook_url.update!(events: ['template.updated'])
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
'event_uuid' => SecureRandom.uuid)
expect(WebMock).not_to have_requested(:post, webhook_url.url)
end
it 'sends again if the response status is 400 or higher' do
stub_request(:post, webhook_url.url).to_return(status: 401)
event_uuid = SecureRandom.uuid
expect do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
'event_uuid' => event_uuid)
end.to change(described_class.jobs, :size).by(1)
expect(WebMock).to have_requested(:post, webhook_url.url).once
args = described_class.jobs.last['args'].first
expect(args['attempt']).to eq(1)
expect(args['last_status']).to eq(401)
expect(args['event_uuid']).to eq(event_uuid)
expect(args['webhook_url_id']).to eq(webhook_url.id)
expect(args['template_id']).to eq(template.id)
end
it "doesn't send again if the max attempts is reached" do
stub_request(:post, webhook_url.url).to_return(status: 401)
expect do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id,
'event_uuid' => SecureRandom.uuid, 'attempt' => 11)
end.not_to change(described_class.jobs, :size)
expect(WebMock).to have_requested(:post, webhook_url.url).once
end
end
end

@ -23,7 +23,7 @@ RSpec.describe 'Sign In' do
fill_in 'Password', with: 'wrong_password' fill_in 'Password', with: 'wrong_password'
click_button 'Sign In' click_button 'Sign In'
expect(page).to have_content('Invalid Email or password') expect(page).to have_content('Invalid email or password')
expect(page).not_to have_content('Document Templates') expect(page).not_to have_content('Document Templates')
end end
end end
@ -51,7 +51,7 @@ RSpec.describe 'Sign In' do
fill_in 'Two-Factor Code from Authenticator App', with: '123456' fill_in 'Two-Factor Code from Authenticator App', with: '123456'
click_button 'Sign In' click_button 'Sign In'
expect(page).to have_content('Invalid Email or password') expect(page).to have_content('Invalid email or password')
expect(page).not_to have_content('Document Templates') expect(page).not_to have_content('Document Templates')
end end
end end

Loading…
Cancel
Save