feat: redesign email templates with professional layout

- Replace bare layout with table-based 600px card design
  - Gray background, white content card, rounded corners
  - Company logo in header (falls back to account name)
  - Attribution footer inlined with subtle styling
- Add reusable _email_button partial (blue CTA, table-based)
- Update invitation, completed, documents_copy emails to use button
- Auto-convert submitter link to styled button in custom email bodies
- Remove deprecated _mailer_attribution and _email_attribution partials
- Preserve RTL support and all existing customization flows
pull/681/head
Sebastian Noe 1 month ago
parent 51ced5ef1e
commit 29378597b3

@ -3,10 +3,60 @@
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style></style>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
color: #1a1a1a;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
p {
margin: 0 0 16px 0;
}
a {
color: #1a73e8;
}
</style>
</head>
<body dir="<%= TextUtils.rtl?(yield) ? 'rtl' : 'auto' %>">
<%= yield %>
<%= render partial: 'shared/mailer_attribution' %>
<body style="margin: 0; padding: 0; background-color: #f4f4f5;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f4f4f5;">
<tr>
<td style="padding: 32px 16px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="max-width: 600px; width: 100%; margin: 0 auto; background-color: #ffffff; border-radius: 8px; border: 1px solid #e4e4e7;">
<%# Header with logo %>
<tr>
<td style="padding: 24px 32px; border-bottom: 1px solid #e4e4e7;">
<% if @current_account&.logo&.attached? %>
<img src="<%= url_for(@current_account.logo) %>" alt="<%= @current_account.name %>" style="max-height: 48px; max-width: 200px; display: block;">
<% else %>
<span style="font-size: 18px; font-weight: 600; color: #1a1a1a;"><%= @current_account&.name || Docuseal.product_name %></span>
<% end %>
</td>
</tr>
<%# Content %>
<tr>
<td dir="<%= TextUtils.rtl?(yield) ? 'rtl' : 'auto' %>" style="padding: 24px 32px; font-size: 15px; line-height: 1.6; color: #1a1a1a;">
<%= yield %>
</td>
</tr>
<%# Footer %>
<tr>
<td style="padding: 16px 32px; border-top: 1px solid #e4e4e7; text-align: center;">
<p style="margin: 0; font-size: 12px; line-height: 1.5; color: #71717a;">
<% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

@ -1,10 +0,0 @@
<p>
---
</p>
<p>
<% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %>
</p>

@ -0,0 +1,9 @@
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" style="margin: 24px auto;">
<tr>
<td style="border-radius: 6px; background-color: #1a73e8;">
<a href="<%= local_assigns[:url] %>" target="_blank" style="display: inline-block; padding: 14px 32px; font-size: 16px; font-weight: 600; color: #ffffff; text-decoration: none; border-radius: 6px;">
<%= local_assigns[:label] %>
</a>
</td>
</tr>
</table>

@ -1 +0,0 @@
<%= render 'shared/email_attribution' %>

@ -1 +1,11 @@
<%= MarkdownToHtml.call(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])) %>
<% submitter = local_assigns[:submitter] %>
<% submitter_url_pattern = submitter&.slug.present? ? "/s/#{submitter.slug}" : nil %>
<% rendered_html = MarkdownToHtml.call(ReplaceEmailVariables.call(local_assigns[:content], submitter: submitter, sig: local_assigns[:sig])) %>
<% if submitter_url_pattern && rendered_html.include?(submitter_url_pattern) %>
<% button_label = I18n.t(submitter.with_signature_fields? ? :review_and_sign : :review_and_submit) %>
<% rendered_html = rendered_html.gsub(%r{<a href="([^"]*#{Regexp.escape(submitter_url_pattern)}[^"]*)">[^<]*</a>}i) do
url = Regexp.last_match(1)
render(partial: 'shared/email_button', locals: { url: url, label: button_label })
end %>
<% end %>
<%= rendered_html.html_safe %>

@ -3,5 +3,5 @@
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(:name_has_been_completed_by_submitters, name: @submitter.submission.name || @submitter.submission.template.name, submitters: @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.uniq.join(', ')) %></p>
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
<%= render partial: 'shared/email_button', locals: { url: submission_url(@submitter.submission), label: I18n.t('view') } %>
<% end %>

@ -4,9 +4,7 @@
<p><%= t('hi_there') %>,</p>
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>
<p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p>
<p>
<%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>
</p>
<%= render partial: 'shared/email_button', locals: { url: submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact), label: @submitter.submission.name || @submitter.submission.template.name } %>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>

@ -1,12 +1,12 @@
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<%= render partial: 'shared/email_button', locals: { url: submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])), label: I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit) } %>
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<%= render partial: 'shared/email_button', locals: { url: submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])), label: I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit) } %>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>

Loading…
Cancel
Save