email editor

This commit is contained in:
Alex Turchyn
2025-07-24 09:41:57 +03:00
committed by GitHub
parent 149e4be07a
commit 561ac63c93
12 changed files with 454 additions and 59 deletions
+2
View File
@@ -37,6 +37,7 @@ import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -109,6 +110,7 @@ safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
+131
View File
@@ -0,0 +1,131 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
let loaderPromise = null
function loadCodeMirror () {
if (!loaderPromise) {
loaderPromise = Promise.all([
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
]).then(([view, commands, language, html, htmlflow]) => {
return {
minimalSetup: [
commands.history(),
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
],
EditorView: view.EditorView,
html: html.html,
htmlflow: htmlflow.default || htmlflow
}
})
}
return loaderPromise
}
export default targetable(class extends HTMLElement {
static [target.static] = [
'codeViewTab',
'previewViewTab',
'editorContainer',
'previewIframe'
]
connectedCallback () {
this.mount()
if (this.input.value) {
this.showPreviewView()
} else {
this.showCodeView()
}
this.previewViewTab.addEventListener('click', this.showPreviewView)
this.codeViewTab.addEventListener('click', this.showCodeView)
}
showCodeView = () => {
this.editorView.dispatch({
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
})
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
this.previewViewTab.classList.add('pb-[3px]')
this.codeViewTab.classList.remove('pb-[3px]')
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.remove('hidden')
this.previewIframe.classList.add('hidden')
}
showPreviewView = () => {
this.previewIframe.srcdoc = this.input.value
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
this.codeViewTab.classList.add('pb-[3px]')
this.previewViewTab.classList.remove('pb-[3px]')
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.add('hidden')
this.previewIframe.classList.remove('hidden')
}
async mount () {
this.input = this.querySelector('input[type="hidden"]')
this.input.style.display = 'none'
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
this.editorView = new EditorView({
doc: this.input.value,
parent: this.editorContainer,
extensions: [
html(),
minimalSetup,
EditorView.lineWrapping,
EditorView.updateListener.of(update => {
if (update.docChanged) this.input.value = update.state.doc.toString()
}),
EditorView.theme({
'&': {
backgroundColor: 'white',
color: 'black',
fontSize: '14px',
fontFamily: 'monospace'
},
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
borderRadius: '0.375rem',
border: 'none'
},
'.cm-gutters': {
display: 'none'
}
})
]
})
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
this.previewIframe.onload = () => {
const previewIframeDoc = this.previewIframe.contentDocument
if (previewIframeDoc.body) {
previewIframeDoc.body.contentEditable = true
}
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
contentDocument.body.addEventListener('input', async () => {
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
const prettifiedHtml = await htmlflow(html)
this.input.value = prettifiedHtml
})
}
}
})
+7 -2
View File
@@ -30,6 +30,7 @@ class SubmitterMailer < ApplicationMailer
@submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_invitation', @submitter)
@@ -71,7 +72,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_completed', @submitter)
@@ -127,7 +128,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
@@ -246,4 +247,8 @@ class SubmitterMailer < ApplicationMailer
user.friendly_name
end
end
def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil
end
end
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
@@ -0,0 +1,11 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -18,17 +18,7 @@
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
@@ -1,14 +1,13 @@
<% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %>
<%= 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]) %>
<% 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')) %></p>
<% 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: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>
@@ -1,7 +1,7 @@
<p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p>
<p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>