Carica oggetto e corpo dell'email appena si sceglie un template A/B.
CI / scan_ruby (push) Failing after 10m36s
CI / scan_js (push) Successful in 10m31s
CI / lint (push) Failing after 11m0s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 23:24:39 +02:00
co-authored by Cursor
parent c4e5f289cf
commit 4ddf534a0c
5 changed files with 150 additions and 29 deletions
+5 -6
View File
@@ -166,19 +166,18 @@ class MailingsController < ApplicationController
end end
def apply_template_if_needed def apply_template_if_needed
copy_template(params.dig(:mailing, :mail_template_id), :subject, :body_html, params[:use_template_content]) copy_template_if_blank(params.dig(:mailing, :mail_template_id), :subject, :body_html)
copy_template(params.dig(:mailing, :mail_template_b_id), :subject_b, :body_html_b, params[:use_template_b_content]) copy_template_if_blank(params.dig(:mailing, :mail_template_b_id), :subject_b, :body_html_b)
end end
def copy_template(template_id, subject_attr, body_attr, flag) def copy_template_if_blank(template_id, subject_attr, body_attr)
return if template_id.blank? return if template_id.blank?
return unless ActiveModel::Type::Boolean.new.cast(flag)
template = MailTemplate.for_project(current_project).find_by(id: template_id) template = MailTemplate.for_project(current_project).find_by(id: template_id)
return unless template return unless template
@mailing.public_send("#{subject_attr}=", template.subject) @mailing.public_send("#{subject_attr}=", template.subject) if @mailing.public_send(subject_attr).blank?
@mailing.public_send("#{body_attr}=", template.body_html) @mailing.public_send("#{body_attr}=", template.body_html) if @mailing.public_send(body_attr).blank?
end end
def rebuild_recipients_if_needed def rebuild_recipients_if_needed
+4
View File
@@ -21,6 +21,10 @@ module ApplicationHelper
"w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none focus:ring-1 focus:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500 dark:focus:ring-zinc-500" "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none focus:ring-1 focus:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500 dark:focus:ring-zinc-500"
end end
def mail_templates_picker_payload(templates)
Array(templates).map { |template| { id: template.id, subject: template.subject, body_html: template.body_html } }
end
def card_class def card_class
"rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100" "rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
end end
@@ -0,0 +1,74 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["select", "subject"]
static values = { templates: Array }
connect() {
this.pendingHtml = null
this.onEditorReady = this.onEditorReady.bind(this)
this.editorElement?.addEventListener("trix-initialize", this.onEditorReady)
if (this.shouldPrefill()) this.apply()
}
disconnect() {
this.editorElement?.removeEventListener("trix-initialize", this.onEditorReady)
}
apply() {
const template = this.currentTemplate()
if (!template) return
if (this.hasSubjectTarget) {
this.subjectTarget.value = template.subject || ""
this.subjectTarget.dispatchEvent(new Event("input", { bubbles: true }))
}
this.setBody(template.body_html || "")
}
shouldPrefill() {
if (!this.selectTarget.value) return false
const subjectBlank = !this.hasSubjectTarget || this.subjectTarget.value.trim() === ""
return subjectBlank && this.bodyIsBlank()
}
currentTemplate() {
const id = Number(this.selectTarget.value)
if (!id) return null
return (this.templatesValue || []).find((template) => Number(template.id) === id) || null
}
setBody(html) {
this.pendingHtml = html
const editor = this.editorElement?.editor
if (editor) {
editor.loadHTML(html)
this.pendingHtml = null
return
}
if (this.hiddenInput) this.hiddenInput.value = html
}
onEditorReady() {
if (this.pendingHtml != null) {
this.editorElement.editor?.loadHTML(this.pendingHtml)
this.pendingHtml = null
return
}
if (this.shouldPrefill()) this.apply()
}
bodyIsBlank() {
const value = this.hiddenInput?.value || ""
return value.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").trim() === ""
}
get editorElement() {
return this.element.querySelector("trix-editor")
}
get hiddenInput() {
return this.element.querySelector("[data-wysiwyg-target='input']")
}
}
+18 -23
View File
@@ -37,7 +37,7 @@
<%= f.check_box :ab_test, { data: { ab_test_target: "checkbox", action: "ab-test#toggle" } } %> <%= f.check_box :ab_test, { data: { ab_test_target: "checkbox", action: "ab-test#toggle" } } %>
<span> <span>
<span class="font-medium">Test A/B su questa comunicazione</span> <span class="font-medium">Test A/B su questa comunicazione</span>
<span class="mt-1 block text-xs text-zinc-500">Due versioni di oggetto e HTML. Ogni destinatario riceve A o B.</span> <span class="mt-1 block text-xs text-zinc-500">Due versioni della stessa email. Carica un template A e un template B nelle colonne: ogni destinatario ne riceve una.</span>
</span> </span>
</label> </label>
@@ -48,48 +48,43 @@
</div> </div>
</div> </div>
<% templates_payload = mail_templates_picker_payload(@mail_templates) %>
<div class="grid gap-6 <%= "lg:grid-cols-2" if mailing.ab_test? %>" data-ab-test-target="grid"> <div class="grid gap-6 <%= "lg:grid-cols-2" if mailing.ab_test? %>" data-ab-test-target="grid">
<div class="space-y-4"> <%= tag.div class: "space-y-4", data: { controller: "template-picker", template_picker_templates_value: templates_payload } do %>
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante A</h2> <h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante A</h2>
<div> <div>
<label class="mb-1 block text-sm font-medium">Template A (opzionale)</label> <label class="mb-1 block text-sm font-medium">Template A</label>
<%= f.collection_select :mail_template_id, @mail_templates, :id, :name, { include_blank: "Nessuno" }, { class: input_class } %> <%= f.collection_select :mail_template_id, @mail_templates, :id, :name,
{ include_blank: "Nessuno — scrivi a mano" },
{ class: input_class, data: { template_picker_target: "select", action: "change->template-picker#apply" } } %>
<p class="mt-1 text-xs text-zinc-500">Scegliendo un template, oggetto e testo si copiano qui sotto. Poi puoi modificarli solo per questo invio.</p>
</div> </div>
<% unless mailing.new_record? %>
<label class="flex items-center gap-2 text-sm">
<%= check_box_tag :use_template_content, "1", false %>
Sostituisci oggetto e HTML A dal template
</label>
<% end %>
<div> <div>
<label class="mb-1 block text-sm font-medium">Oggetto A</label> <label class="mb-1 block text-sm font-medium">Oggetto A</label>
<%= f.text_field :subject, required: true, class: input_class %> <%= f.text_field :subject, required: true, class: input_class, data: { template_picker_target: "subject" } %>
</div> </div>
<div> <div>
<%= render "shared/wysiwyg_field", form: f, method: :body_html, label: "Corpo HTML A" %> <%= render "shared/wysiwyg_field", form: f, method: :body_html, label: "Corpo HTML A" %>
</div> </div>
</div> <% end %>
<div class="space-y-4 <%= "hidden" unless mailing.ab_test? %>" data-ab-test-target="panel"> <%= tag.div class: "space-y-4 #{'hidden' unless mailing.ab_test?}", data: { controller: "template-picker", template_picker_templates_value: templates_payload, ab_test_target: "panel" } do %>
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante B</h2> <h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante B</h2>
<div> <div>
<label class="mb-1 block text-sm font-medium">Template B (opzionale)</label> <label class="mb-1 block text-sm font-medium">Template B</label>
<%= f.collection_select :mail_template_b_id, @mail_templates, :id, :name, { include_blank: "Nessuno" }, { class: input_class } %> <%= f.collection_select :mail_template_b_id, @mail_templates, :id, :name,
{ include_blank: "Nessuno — scrivi a mano" },
{ class: input_class, data: { template_picker_target: "select", action: "change->template-picker#apply" } } %>
<p class="mt-1 text-xs text-zinc-500">Stesso meccanismo della variante A: il template riempie oggetto e corpo B.</p>
</div> </div>
<% unless mailing.new_record? %>
<label class="flex items-center gap-2 text-sm">
<%= check_box_tag :use_template_b_content, "1", false %>
Sostituisci oggetto e HTML B dal template
</label>
<% end %>
<div> <div>
<label class="mb-1 block text-sm font-medium">Oggetto B</label> <label class="mb-1 block text-sm font-medium">Oggetto B</label>
<%= f.text_field :subject_b, class: input_class %> <%= f.text_field :subject_b, class: input_class, data: { template_picker_target: "subject" } %>
</div> </div>
<div> <div>
<%= render "shared/wysiwyg_field", form: f, method: :body_html_b, label: "Corpo HTML B" %> <%= render "shared/wysiwyg_field", form: f, method: :body_html_b, label: "Corpo HTML B" %>
</div> </div>
</div> <% end %>
</div> </div>
<div> <div>
@@ -104,4 +104,53 @@ class MailingsControllerTest < ActionDispatch::IntegrationTest
body = (mail.html_part || mail).body.to_s body = (mail.html_part || mail).body.to_s
assert_match(/Versione B/, body) assert_match(/Versione B/, body)
end end
test "new mailing form embeds template content for the picker" do
login_as users(:admin)
follow_redirect! if response.redirect?
template = MailTemplate.create!(
project: @project,
name: "Variante B picker",
subject: "Oggetto dal template B",
body_html: "<p>Corpo dal template B</p>"
)
get new_mailing_path(project_code: @project.code)
assert_response :success
assert_match(/template-picker/, response.body)
assert_match(/Oggetto dal template B/, response.body)
assert_match(/Corpo dal template B/, response.body)
assert_includes response.body, template.id.to_s
end
test "create copies blank variant B fields from selected template" do
login_as users(:admin)
follow_redirect! if response.redirect?
template_b = MailTemplate.create!(
project: @project,
name: "Template B server",
subject: "B {{societa}}",
body_html: "<p>Versione B dal template</p>"
)
post mailings_path(project_code: @project.code), params: {
mailing: {
name: "AB da template",
mail_identity_id: @identity.id,
mail_template_b_id: template_b.id,
audience: "to_send",
ab_test: "1",
ab_assignment: "from_record",
subject: "Oggetto A",
body_html: "<p>Versione A</p>",
subject_b: "",
body_html_b: "",
interval_seconds: 0
}
}
mailing = Mailing.last
assert_redirected_to mailing_path(mailing, project_code: @project.code)
assert_equal "B {{societa}}", mailing.subject_b
assert_includes mailing.body_html_b, "Versione B dal template"
end
end end