widget:ticketdata: function extention to handle Email sending

* remap to use `policy code`
* adapt the constants
* start with clear_form()
* use service 'send_ticketdata' which crate lettre
* enable smtp authorization via credentilas
* support html using crate 'maud'
* provide the needed structures
* update the constants for property and entity assigns
* include send Email service (sendticketdata)
* send_ticketdata: include html  maud modules

Signed-off-by: Ralf Zerres <ralf.zerres@networkx.de>
This commit is contained in:
2021-03-03 22:37:57 +01:00
parent 1af299dc01
commit aeaafd8c57
5 changed files with 213 additions and 112 deletions

View File

@@ -10,10 +10,14 @@ pub static PROP_ADVOTRACKER: &str = "advotracker";
pub static PROP_MAIL_CC_1: &str = "advotracker@hiedemann.de";
pub static PROP_MAIL_CC_2: &str = "service@hiedemann.de";
pub static PROP_MAIL_BCC_1: &str = "schloemer@hiedemann.de";
pub static PROP_MAIL_BCC_2: &str = "support@networkx.de";
pub static PROP_MAIL_TO_1: &str = "allianz@ponschab-partner.com";
pub static PROP_MAIL_TO_2: &str = "kontakt@chevalier.law";
pub static PROP_MAIL_TO_3: &str = "kontakt@metamedlaw.de";
pub static PROP_MAIL_TO_4: &str = "sekretariat@m2-mediation.de";
pub static PROP_MAIL_FROM: &str = "advotracker@heidemann.de";
pub static PROP_MAIL_SUBJECT: &str = "ZMB Allianz - neues Mandat";
pub static PROP_POLICY_CHECK: &str = "policy_check";
pub static PROP_POLICY_PROGRESS_COUNT: &str = "policy_progress_count";
@@ -91,6 +95,7 @@ pub static ID_POLICY_CHECK_LABEL_HINT: &str = "policy_check_label_hint";
pub static ID_POLICY_CHECK_LABEL_MENU: &str = "policy_check_label_menu";
pub static ID_POLICY_CHECK_LABEL_POLICY_NUMBER: &str = "policy_check_label_policy_number";
pub static ID_POLICY_CHECK_LABEL_RESULT: &str = "policy_check_label_result";
pub static ID_POLICY_CHECK_POLICY_CODE: &str = "policy_check_policy_code";
pub static ID_POLICY_CHECK_POLICY_NUMBER: &str = "policy_number";
pub static ID_POLICY_CHECK_POPUP_PROGRESS: &str = "policy_check_popup_progress";
pub static ID_POLICY_CHECK_PROGRESS_BAR: &str = "policy_check_progress_bar";
@@ -123,8 +128,6 @@ pub static ID_TICKET_DATA_BUTTON_MENU: &str = "ticket_data_button_menu";
pub static ID_TICKET_DATA_BUTTON_RESULT: &str = "ticket_data_button_result";
pub static ID_TICKET_DATA_CALLBACK_DATE: &str = "ticket_data_label_callback_date";
pub static ID_TICKET_DATA_CALLBACK_NUMBER: &str = "ticket_data_label_callback_number";
pub static ID_TICKET_DATA_COMBO_BOX_MAIL_TO: &str = "ticket_data_combo_box_mail_to";
pub static ID_TICKET_DATA_COMBO_BOX_MAIL_CC: &str = "ticket_data_combo_box_mail_cc";
pub static ID_TICKET_DATA_CONTAINER_MAIL: &str = "ticket_data_container_mail";
pub static ID_TICKET_DATA_COUNT_BLOCK: &str = "ticket_data_count_block";
pub static ID_TICKET_DATA_DEDUCTIBLE: &str = "ticket_data_deductible";
@@ -146,13 +149,13 @@ pub static ID_TICKET_DATA_LABEL_IVR_COMMENT: &str = "ticket_data_label_ivr_comme
pub static ID_TICKET_DATA_LABEL_MAIL_CC: &str = "ticket_data_label_mail_cc";
pub static ID_TICKET_DATA_LABEL_MAIL_TO: &str = "ticket_data_label_mail_to";
pub static ID_TICKET_DATA_LABEL_MENU: &str = "ticket_data_label_menu";
pub static ID_TICKET_DATA_LABEL_POLICY_CODE: &str = "ticket_data_label_policy_code";
pub static ID_TICKET_DATA_LABEL_POLICY_HOLDER: &str = "ticket_data_label_policy_holder";
pub static ID_TICKET_DATA_LABEL_POLICY_NUMBER: &str = "ticket_data_label_policy_number";
pub static ID_TICKET_DATA_LOGO_BAR: &str = "ticket_data_logo_bar";
pub static ID_TICKET_DATA_MAIL_CC: &str = "ticket_data_mail_cc";
pub static ID_TICKET_DATA_MAIL_TO: &str = "ticket_data_mail_to";
pub static ID_TICKET_DATA_POLICY_CODE: &str = "ticket_data_policy_code";
pub static ID_TICKET_DATA_POLICY_HOLDER: &str = "ticket_data_policy_holder";
pub static ID_TICKET_DATA_POLICY_NUMBER: &str = "ticket_data_policy_number";
pub static ID_TICKET_DATA_RESULT: &str = "ticket_data_result";
pub static ID_TICKET_DATA_VIEW: &str = "ticket_data_view";
pub static ID_TICKET_DATA_WIDGET: &str = "ticket_data_widget";

View File

@@ -110,6 +110,37 @@ pub struct CsvExportRecord {
pub ivr_comment: String,
}
/// Handel fields of an Email (header, body)
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct Email {
/// recipient address
pub mail_to: String,
/// Carbon Copy recipient address
pub mail_cc: String,
/// Blind Carbon Copy recipient address
pub mail_bcc: String,
/// Sender address
pub mail_from: String,
/// Mail subject
pub subject: String,
/// Body policy code
pub policy_code: String,
/// Body type policy holder
pub policy_holder: String,
/// Body type deductible
pub deductible: String,
/// Body callback number
pub callback_number: String,
/// Body type callback date
pub callback_date: String,
/// Body type harm type
pub harm_type: String,
/// Body type ivr comment
pub ivr_comment: String
}
impl Email {}
/// Harm data are list/collections of harm types. You may toggle them to an unselected state.
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct HarmData {
@@ -157,20 +188,6 @@ pub struct PolicyCheck {
impl PolicyCheck {}
// #[derive(Default, Clone, Debug, Serialize, Deserialize)]
// pub struct PolicyCheckList {
// pub title: String,
// pub list: Vec<PolicyCheck>
// }
// impl PolicyCheckList {
// pub fn new(title: impl Into<String>) -> Self {
// PolicyCheckList {
// title: title.into(),
// ..Default::default()
// }
// }
/// Structure collecting policy data elements
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct PolicyList {

View File

@@ -6,56 +6,115 @@
*/
use lettre::{
message::{header, MultiPart, SinglePart},
Message, SmtpTransport, Transport,
transport::smtp::authentication::Credentials,
//Message, SmtpTransport, Transport,
Message, SmtpTransport,
};
use locales::t;
use maud::html;
use std::error::Error;
//use std::process;
use tracing::{info, trace};
use tracing::{info, error, trace};
use crate::data::structures::Email;
/// send ticket data via eMail
pub fn sendticketdata(recipient: &str, body: &str, lang: &str) -> Result<(), Box<dyn Error>> {
pub fn sendticketdata(email: &Email, lang: &str) -> Result<(), Box<dyn Error>> {
let mut res = t!("sendticketdata.export.started", lang);
let mut state = t!("state.started", lang);
trace!(target: "sendticketdata", process = ?res, state = ?state);
let email = Message::builder()
.from("Advotracker Hiedemann <info@hiedemann.de>".parse().unwrap())
.reply_to("Support <support@networkx.de>".parse().unwrap())
//.to("Ralf Zerres <ralf.zerres@networkx.de>".parse().unwrap())
.to(recipient.parse().unwrap())
.subject("Advotracker Ticket")
//.body("Eine neue advotracker eMail!".to_string())
.body(body.to_string())
.unwrap();
// The html we want to send.
// It uses https://crates.io/crates/maud
let html = html! {
head {
title { (email.subject) " (" (email.policy_code) ")" }
style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
// compose with variables and strings
h2 { (email.subject) " (" (email.policy_code) ")" }
p { "Policen-Code: " (email.policy_code) }
p { "Policen-Code: " (email.policy_code) }
p { "Versicherungsnehmer: " (email.policy_holder) }
p { "Selbstbehalt: "(email.deductible) }
p { "Rückrufnummer: " (email.callback_number) }
p { "Rückrufdatum: " (email.callback_date) }
p { "Schadenstyp: "(email.harm_type) }
p { "IVR Kommentar: "(email.ivr_comment) }
}
};
info!("emal: {:?}", email);
let ascii_body = String::new()
+ &"Policen-Code: ".to_string() + &(email.policy_code) + &"\n"
+ &"Versicherungsnehmer: ".to_string() + &(email.policy_holder) + &"\n"
+ &"Selbstbehalt: ".to_string() + &(email.deductible) + &"\n"
+ &"Rückrufnummer: ".to_string()+ &(email.callback_number) + &"\n"
+ &"Rückrufdatum: ".to_string() + &(email.callback_date) + &"\n"
+ &"Schadenstyp: ".to_string() + &(email.harm_type) + &"\n"
+ &"IVR Kommentar: ".to_string() + &(email.ivr_comment) + &"\n";
// WIP testing ends here
//process::exit(0);
info!("email body: {:?}", ascii_body);
let message = Message::builder()
.from((email.mail_from).parse().unwrap())
//.reply_to("Support <support@networkx.de>".parse().unwrap())
.to("Networkx-Info <info@networkx.de>".parse().unwrap())
.cc("Ralf Zerres <ralf.zerres@networkx.de>".parse().unwrap())
.reply_to((email.mail_from).parse().unwrap())
//.to((email.mail_to).parse().unwrap())
//.cc((email.mail_cc).parse().unwrap())
//.bcc((email.mail_bcc).parse().unwrap())
.subject(String::new()
+ &email.subject.to_string()
+ &" (".to_string()
+ &email.policy_code.to_string()
+ &")".to_string()
)
.multipart(
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.body(String::from(ascii_body)),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(html.into_string()),
),
)
.expect("failed to build email");
info!("message: {:?}", message);
// Create credential for remote authentication (username, password)
//let creds = Credentials::new("ralf.zerres@networkx.de".to_string(), "dekifjgh".to_string());
// WIP: get credentials from config file / environment
let creds = Credentials::new("ralf.zerres.de@gmail.com".to_string(), "20jacara03".to_string());
// Open a remote connection to relay server
let _mailer = SmtpTransport::relay("smtp.gmail.com")
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
// match mailer.send(&email) {
// Ok(_) => info!("Email sent successfully!"),
// Err(e) => error!("Could not send email: {:?}", e),
// }
match mailer.send(&message) {
Ok(_) => info!("Email sent successfully!"),
Err(e) => error!("Could not send email: {:?}", e),
}
trace!(target: "sendticketdata", recipient = ?recipient);
trace!(target: "sendticketdata", email = ?email);
state = t!("state.finished", lang);
res = t!("csv.export.finished", lang);
trace!(target: "csv-export", process = ?res, state = ?state);
res = t!("sendticketdata.export.finished", lang);
trace!(target: "sendticketdata", process = ?res, state = ?state);
Ok(())
}

View File

@@ -15,9 +15,9 @@ use std::time::SystemTime;
use tracing::{info, trace};
use crate::{
data::constants::*,
data::{constants::*, structures::Email},
widgets::global_state::GlobalState,
//services::exports::sendticketdata,
services::exports::send_ticketdata::sendticketdata,
widgets::policycheck::policycheck_state::{PolicycheckAction, PolicycheckState},
};
@@ -33,20 +33,9 @@ pub enum TicketdataAction {
RemoveFocus(Entity),
SetToggleTheme(Entity),
SetEntry(Entity),
SendForm(),
SetVisibility(Entity),
SendForm(), SetVisibility(Entity),
TextChanged(Entity, usize),
UpdatePolicyNumber(String)
}
/// Handel fields of an Email (header, body)
#[derive(Clone, PartialEq)]
pub struct Email {
recipient_to: String,
recipient_cc: String,
sender: String,
subject: String,
body: String,
UpdatePolicyCode(String)
}
/// Define valid environment variables provided via .env files
@@ -72,43 +61,79 @@ impl GlobalState for TicketdataState {}
/// Method definitions, that react on any given state change inside the `Ticketdata` widget.
impl TicketdataState {
/// Clear text in text box.
pub fn clear_entry(entity: Entity, ctx: &mut Context<'_>) {
/// Clear the text property of all children of the given form entity
pub fn clear_form(entity: Entity, ctx: &mut Context<'_>) {
if let Some(count) = ctx.get_widget(entity).children_count() {
println!("Widget name: {:?}", ctx.get_widget(entity).get::<String>("name"));
println!("Widget id: {:?}", ctx.get_widget(entity).get::<String>("id"));
info!("Widget name: {:?}", ctx.get_widget(entity).get::<String>("name"));
info!("Widget id: {:?}", ctx.get_widget(entity).get::<String>("id"));
}
// identify the form by its id
if let form_entity = ctx.child(ID_TICKET_DATA_GRID).entity() {
info!("Form id: {:?}", ctx.get_widget(form_entity).get::<String>("id"));
info!("Form node name: {:?}", ctx.get_widget(form_entity).get::<String>("name"));
// Loop through children
if let Some(count) = ctx.get_widget(form_entity).children_count() {
for c in 1..=count {
info!("WIP clear entry of child {:?}", c);
//info!("WIP clear entry of child {:?}: {:?}", c, ctx.get_child(form_entity).get::<String>("name"));
}
}
}
// let Some(count) = ctx.child(ID_TICKET_DATA_GRID).entitry().children_count() {
// for c in 1..=count {
// println!("WIP clear entry of child {:?}", c);
// }
// }
//let mut children = vec![];
//get_all_children(&mut children, entity, ecm.entity_store());
//trace!("Children {:?}: {:?}", children.len(), children);
//println!("Child name: {:?}", ctx.try_child("ticket_data_form"));
//TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_HOLDER), "");
println!("Number of childs: {:?}", count);
for c in 1..=count {
println!("WIP clear entry of child {:?}", c);
//println!("Number of childs: {:?}", count);
//for c in 1..=count {
// println!("WIP clear entry of child {:?}", c);
// children = get_all_children()
//println!("Child {:?}: Entity: {:?}", c, ctx.child_from_index(c));
//println!("Text: {:?}", ctx.get_widget(entity).entity_of_child(entity));
//TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_HOLDER), "");
}
}
//}
//TextBox::text_set(&mut ctx.widget(entity), String::from(""));
}
pub fn send_form(entity: Entity, _ctx: &mut Context<'_>) {
info!("WIP: Sending form to construct eMail for {:?}", entity);
}
pub fn send_form(entity: Entity, ctx: &mut Context<'_>, lang: &str) {
/// sending message 'ClearForm'
pub fn send_message_clear_form(&mut self) {
info!("Sending message 'TicketdataAction::ClearForm'");
self.actions.push(TicketdataAction::ClearForm());
}
// type conversion (String -> u64)
//let policy_code = ctx.child(ID_TICKET_DATA_POLICY_CODE).get::<String>("text").unwrap().parse::<u64>().unwrap();
/// sending message 'SendForm'
pub fn send_message_send_form(&mut self) {
info!("Sending message 'TicketdataAction::SendForm'");
self.actions.push(TicketdataAction::SendForm());
// WIP: get selected items ComboBox'es
//let mail_cc_index = *TicketdataView::selected_index_ref(&ctx.widget()) as usize;
//let mail_cc_selected = TicketdataView::mail_cc_ref(&ctx.widget())[mail_cc_index].clone();
// create Email structures
let email = Email {
// WIP: mail_to -> selected index auslesen
//mail_to: ctx.child(ID_TICKET_DATA_MAIL_TO).get::<String>("text").to_string(),
// WIP: mail_cc -> selected index auslesen
//mail_cc: ctx.child(ID_TICKET_DATA_MAIL_CC).get::<String>("text").to_string(),
mail_to: PROP_MAIL_TO_1.to_string(),
mail_cc: PROP_MAIL_CC_1.to_string(),
mail_bcc: PROP_MAIL_BCC_1.to_string(),
mail_from: PROP_MAIL_FROM.to_string(),
subject: PROP_MAIL_SUBJECT.to_string(),
policy_code: ctx.child(ID_TICKET_DATA_POLICY_CODE).get::<String>("text").to_string(),
policy_holder: ctx.child(ID_TICKET_DATA_POLICY_HOLDER).get::<String>("text").to_string(),
deductible: ctx.child(ID_TICKET_DATA_DEDUCTIBLE).get::<String>("text").to_string(),
callback_number: ctx.child(ID_TICKET_DATA_CALLBACK_NUMBER).get::<String>("text").to_string(),
callback_date: ctx.child(ID_TICKET_DATA_CALLBACK_DATE).get::<String>("text").to_string(),
harm_type: ctx.child(ID_TICKET_DATA_HARM_TYPE).get::<String>("text").to_string(),
ivr_comment: ctx.child(ID_TICKET_DATA_IVR_COMMENT).get::<String>("text").to_string(),
};
info!("WIP: Sending form to construct eMail to {:?}", email);
// send email via service
let _ = sendticketdata(&email, &lang);
}
}
@@ -143,30 +168,24 @@ impl State for TicketdataState {
_registry: &mut Registry,
ctx: &mut Context<'_>,
) {
for action in messages.read::<TicketdataAction>() {
match action {
for message in messages.read::<TicketdataAction>() {
match message {
TicketdataAction::ClearForm() => {
println!("message: {:?} recieved", action);
info!("message: {:?} recieved", action);
//TicketdataState::clear_entry(ctx.entity(), ctx);
//ctx.clear_children_of(self.ticket_data_form);
info!("message: {:?} recieved", message);
TicketdataState::clear_form(ctx.entity(), ctx);
}
TicketdataAction::SendForm() => {
println!("message: {:?} recieved", action);
info!("message: {:?} recieved", action);
TicketdataState::send_form(ctx.entity(), ctx);
info!("message: {:?} recieved", message);
TicketdataState::send_form(ctx.entity(), ctx, &self.lang);
}
_ => { println!("messages: action not implemented!"); }
}
}
for action in messages.read::<PolicycheckAction>() {
match action {
PolicycheckAction::UpdatePolicyNumber(policy_number) => {
info!("Message received: 'PolicycheckAction::UpdatePolicyNumber({:?})'", policy_number);
//progress_bar.set::<f64>("val", current_progress + amount);
//TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_NUMBER), "9999999992");
TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_NUMBER), policy_number);
for message in messages.read::<PolicycheckAction>() {
match message {
PolicycheckAction::UpdatePolicyCode => {
info!("Message received: 'PolicycheckAction::UpdatePolicyCode'");
//TextBlock::text_set(&mut ctx.child(ID_TICKET_DATA_POLICY_CODE), policy_code);
}
_ => { println!("messages: action not implemented!"); }
}

View File

@@ -12,8 +12,7 @@ use orbtk::{
use crate::{
data::constants::*,
//widgets::ticketdata::ticketdata_state::{TicketdataAction, TicketdataState},
widgets::ticketdata::ticketdata_state::TicketdataState,
widgets::ticketdata::ticketdata_state::{TicketdataAction, TicketdataState},
};
// Macro that initializes the widget structures/variables for the policy check view
@@ -91,6 +90,8 @@ impl Template for TicketdataView {
.style("container_form")
.child(
Grid::new()
.id(ID_TICKET_DATA_GRID)
.name(ID_TICKET_DATA_GRID)
.columns(
Columns::create()
.push("auto") // Label
@@ -117,17 +118,17 @@ impl Template for TicketdataView {
)
.child(
TextBlock::new()
.id(ID_TICKET_DATA_LABEL_POLICY_NUMBER)
.id(ID_TICKET_DATA_LABEL_POLICY_CODE)
.attach(Grid::row(0))
.attach(Grid::column(0))
.text("Policy number")
.text("Policy code")
.h_align("end")
.v_align("center")
.build(ctx),
)
.child(
TextBlock::new()
.id(ID_TICKET_DATA_POLICY_NUMBER)
.id(ID_TICKET_DATA_POLICY_CODE)
.attach(Grid::row(0))
.attach(Grid::column(2))
.text("9999999990")
@@ -149,6 +150,7 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_POLICY_HOLDER)
.attach(Grid::row(2))
.attach(Grid::column(2))
.text("")
.water_mark("Name des Versicherungsnehmers")
.build(ctx),
)
@@ -167,6 +169,7 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_DEDUCTIBLE)
.attach(Grid::row(4))
.attach(Grid::column(2))
.text("")
.water_mark("im Beratungsgespräch erfragen")
.build(ctx),
)
@@ -185,6 +188,7 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_CALLBACK_NUMBER)
.attach(Grid::row(6))
.attach(Grid::column(2))
.text("")
.water_mark("wie zu erreichen")
.build(ctx),
)
@@ -203,6 +207,7 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_CALLBACK_DATE)
.attach(Grid::row(8))
.attach(Grid::column(2))
.text("")
.water_mark("Rückruf gewünscht um")
.build(ctx),
)
@@ -211,17 +216,18 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_LABEL_HARM_TYPE)
.attach(Grid::row(10))
.attach(Grid::column(0))
.water_mark("Harm type")
.text("Harm type")
.h_align("end")
.v_align("center")
.build(ctx),
)
.child(
TextBox::new()
.id(ID_TICKET_DATA_LABEL_HARM_TYPE)
.name(ID_TICKET_DATA_LABEL_HARM_TYPE)
.id(ID_TICKET_DATA_HARM_TYPE)
.name(ID_TICKET_DATA_HARM_TYPE)
.attach(Grid::row(10))
.attach(Grid::column(2))
.text("")
.water_mark("Rechtsproblem/Schadenstyp")
.build(ctx),
)
@@ -240,6 +246,7 @@ impl Template for TicketdataView {
.id(ID_TICKET_DATA_IVR_COMMENT)
.attach(Grid::row(12))
.attach(Grid::column(2))
.text("")
.water_mark("Rechtsrat / Kommentar zu Haftungs, bzw. Deckung")
//.height(48.0)
.build(ctx),
@@ -266,9 +273,7 @@ impl Template for TicketdataView {
.style(STYLE_BUTTON_ACTION)
.text("Clear")
.on_click(move |states, _| {
//println!("WIP: clear from");
//states.send_message(TicketdataAction::ClearForm, id);
states.get_mut::<TicketdataState>(id).send_message_clear_form();
states.send_message(TicketdataAction::ClearForm(), id);
false
})
.build(ctx),
@@ -280,9 +285,7 @@ impl Template for TicketdataView {
.text("Send")
//.visibility(Visibility::Collapsed)
.on_click(move |states, _entity| {
//println!("WIP: send mail");
//states.get_mut::<TicketdataAction>(id).send_message(TicketdataAction::SendForm, id);
states.get_mut::<TicketdataState>(id).send_message_send_form();
states.send_message(TicketdataAction::SendForm(), id);
false
})
.build(ctx),
@@ -325,7 +328,7 @@ impl Template for TicketdataView {
)
.child(
ComboBox::new()
.id(ID_TICKET_DATA_COMBO_BOX_MAIL_TO)
.id(ID_TICKET_DATA_MAIL_TO)
.attach(Grid::row(0))
.attach(Grid::column(3))
.style(STYLE_MAIL_TO)
@@ -371,7 +374,7 @@ impl Template for TicketdataView {
)
.child(
ComboBox::new()
.id(ID_TICKET_DATA_COMBO_BOX_MAIL_CC)
.id(ID_TICKET_DATA_MAIL_CC)
.attach(Grid::row(2))
.attach(Grid::column(3))
.style(STYLE_MAIL_CC)