use super::*;
use crate::{Locale, Staff};
#[cfg(feature = "gui")]
use mkutil::html_scraping::PlaceImageHint;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "mongo", derive(Serialize, Deserialize,))]
pub struct Ticket {
#[cfg_attr(feature = "mongo", serde(skip))]
pub ext: TicketExpand,
#[cfg_attr(
feature = "mongo",
serde(rename = "_id", skip_serializing_if = "Option::is_none")
)]
pub id: Option<ObjectId>,
#[cfg_attr(feature = "mongo", serde(default = "untitled", rename = "subject"))]
pub(super) name: String,
#[cfg_attr(feature = "mongo", serde(rename = "source_type"))]
pub typ: TicketAuthorSource,
pub(super) content: Vec<ObjectId>,
pub(super) topical_assets: Vec<ObjectId>,
#[cfg_attr(feature = "mongo", serde(rename = "soup_format"))]
pub(super) topic: MsgTopic,
pub(super) sender: String,
#[cfg_attr(feature = "mongo", serde(rename = "orig_locale"))]
pub composed_with_locale: Locale,
#[cfg_attr(
feature = "mongo",
serde(
rename = "datetime",
with = "bson::serde_helpers::chrono_datetime_as_bson_datetime"
)
)]
pub(super) created_at: DateTime<Utc>,
pub(super) instantiated_for: Vec<String>,
}
fn untitled() -> String {
String::from("Untitled")
}
impl BsonId for Ticket {
fn bson_id_as_ref(&self) -> Option<&ObjectId> {
self.id.as_ref()
}
fn bson_id(&self) -> AnyResult<&ObjectId> {
self.id.as_ref().context("Ticket without BSON ObjectId")
}
}
impl ReadWriteSuggest for Ticket {
fn write_suggest() -> Self {
Self::empty().with_mode(MediaMode::WriteSuggest)
}
fn with_mode(self, mode: MediaMode) -> Self {
let mut ticket = Self::empty();
ticket.mode_mut(mode);
ticket
}
fn mode(&self) -> &MediaMode {
&self.ext.mode
}
fn mode_mut(&mut self, mode: MediaMode) {
self.ext.mode = mode;
}
#[cfg(feature = "gui")]
fn read_mode_ui(&mut self, _ui: &mut egui::Ui) {
#[cfg(feature = "image_processing")]
if !self.ext.images_loaded {
_ui.separator();
if _ui
.button(
RichText::new("🖳 Read Images")
.background_color(Color32::LIGHT_RED)
.color(Color32::BLACK),
)
.on_hover_text("Load all images for this ticket")
.clicked()
{
self.texture_ids_mut();
};
};
}
#[cfg(feature = "gui")]
fn write_suggest_ui(&mut self, ui: &mut egui::Ui) {
if ui
.button("➕ New Ticket")
.on_hover_text("Add a new ticket for this asset")
.clicked()
{
if self.ext.ticket_draft.is_none() {
self.ext.start_new_composing();
} else {
self.mode_mut(MediaMode::WriteCompose);
};
};
}
}
impl Ticket {
pub fn empty() -> Self {
Self {
topic: MsgTopic::Defect,
created_at: Utc::now(),
..Default::default()
}
}
fn sent_by(mut self, user: Staff) -> Self {
self.sender = user.name_owned();
self
}
fn topical_asset(mut self, topical: &ProductionAsset) -> AnyResult<Self> {
self.topical_assets.push(topical.bson_id()?.clone());
Ok(self)
}
fn created_now(mut self) -> Self {
self.created_at = Utc::now();
self
}
#[cfg(feature = "gui")]
pub fn subtickets_result(
mut self,
subtickets: Result<Vec<SubTicket>, DatabaseError>,
current_username: &str,
chrono_fmt: &str,
layout: &EmbedLayoutPref,
img_hint: &PlaceImageHint,
) -> Self {
self.ext.subtickets = subtickets.map(|subtickets| {
subtickets
.into_iter()
.map(|s| {
SubTicketReadBuilder::new(s).finish(
current_username,
chrono_fmt,
layout,
img_hint,
)
})
.collect()
});
self
}
fn selected_if_any_unclosed_subticket(mut self) -> Self {
self.ext = self.ext.selected_if_any_unclosed_subticket();
self
}
fn preload_tex_ids_if_selected(mut self) -> Self {
if self.ext.selected {
self.texture_ids_mut();
};
self
}
fn convert_local_time(mut self, chrono_fmt: &str) -> Self {
self.ext.created_at_local = local_time(self.created_at, chrono_fmt);
self
}
fn subticket_draft_number(mut self) -> Self {
let next_number = match &self.ext.subtickets {
Ok(subtickets) => subtickets
.iter()
.map(|s| s.number)
.max()
.map(|i| i + 1)
.unwrap_or(1),
Err(_) => 1,
};
self.ext.subticket_draft = self.ext.subticket_draft.number(next_number);
self
}
fn tally(mut self) -> Self {
self.ext = self.ext.tally();
self
}
fn untitled_if_empty(mut self) -> Self {
if self.name.is_empty() {
self.name = untitled();
};
self
}
fn expand(mut self, ext: TicketExpand) -> Self {
self.ext = ext;
self
}
}
impl Ticket {
fn clone_with_composer(&mut self) -> Self {
let ext = std::mem::take(&mut self.ext);
self.clone().expand(ext)
}
pub(super) fn is_draft_unwrap_not_empty(&self) -> bool {
self.ext
.ticket_draft
.as_ref()
.unwrap()
.iter()
.filter(|draft| draft.composer.is_some())
.any(|draft| !draft.composer.inner_as_ref_unwrap().is_empty())
}
pub fn take_draft_unwrap(&mut self) -> Vec<SubTicketCompose> {
self.ext
.ticket_draft
.take()
.unwrap()
.into_iter()
.filter_map(|draft| draft.try_into().ok())
.filter(|draft: &SubTicketCompose| !draft.composer.is_empty())
.collect()
}
pub fn subtickets_ids(&self) -> &Vec<ObjectId> {
&self.content
}
pub fn selected(&self) -> bool {
self.ext.selected
}
pub fn selected_mut(&mut self, selected: bool) {
self.ext.selected = selected;
}
pub fn texture_ids_mut(&mut self) {
if let Ok(subtickets) = self.ext.subtickets.as_mut() {
subtickets.iter_mut().for_each(|s| {
s.texture_ids_mut();
});
#[cfg(feature = "image_processing")]
{
self.ext.images_loaded = true;
}
};
}
fn viewing_locale_mut(&mut self, locale: &Locale) {
if let Ok(subtickets) = self.ext.subtickets.as_mut() {
subtickets.iter_mut().for_each(|s| {
s.viewing_locale_mut(locale);
});
};
self.ext.viewing_locale = locale.clone();
}
fn type_as_str(&self) -> &str {
self.typ.as_ref()
}
pub fn reply_box_width_mut(&mut self, width: f32) {
if let Ok(subtickets) = self.ext.subtickets.as_mut() {
subtickets.iter_mut().for_each(|s| {
s.reply_box_width_mut(width);
});
};
}
}
#[cfg(feature = "gui")]
impl Ticket {
pub fn show_toggle_vis_button(&mut self, ui: &mut egui::Ui) {
if ui
.toggle_value(&mut self.ext.selected, &self.name)
.clicked()
{
#[cfg(feature = "image_processing")]
if !self.ext.images_loaded {
self.texture_ids_mut();
};
};
}
pub fn show_ticket_details(&self, ui: &mut egui::Ui) {
ui.label(format!(
"{} {} Feedback",
self.ext.created_at_local,
self.type_as_str()
));
ui.heading(&self.name);
}
pub fn show_tally(&self, ui: &mut egui::Ui) {
self.ext.tally.ui(ui);
}
pub fn show_viewing_locale_options(&mut self, ui: &mut egui::Ui) {
if let Ok(settings) = ClientCfgCel::settings() {
ui.horizontal(|ui| {
for locale in Locale::iter() {
if settings.locales().contains(&locale.as_ref().to_owned()) {
if ui
.add(egui::RadioButton::new(
self.ext.viewing_locale == locale,
locale.as_ref(),
))
.clicked()
{
self.viewing_locale_mut(&locale)
};
};
}
});
};
}
pub fn edit_metadata_ui(&mut self, ui: &mut egui::Ui) {
ticket_author_source_options_ui(ui, &mut self.typ);
ui.horizontal(|ui| {
ui.label(RichText::new("Ticket Name:").heading().strong());
ui.add(egui::TextEdit::singleline(&mut self.name).desired_width(f32::INFINITY));
});
locale::locale_options_ui(ui, &mut self.composed_with_locale);
}
pub fn discardable_composers_unwrap_ui(&mut self, ui: &mut egui::Ui, palette: &Palette) {
self.ext.discardable_composers_unwrap_ui(ui, palette);
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
self.ext.show_add_subticket_draft_unwrap_button(ui);
});
}
pub fn show_delete_button(&self, ui: &mut egui::Ui, tx: &Sender<TicketAction>) {
if ui
.button("🗑")
.on_hover_text(
RichText::new(format!(
"Delete this whole {} Feedback Ticket",
self.type_as_str()
))
.color(Color32::RED),
)
.clicked()
{
if dialog::confirm_dialog(
&format!("Delete this {} Feedback Ticket?", self.type_as_str()),
"Deleting ticket cannot be undone",
) {
tx.send(TicketAction::DeleteTicket {
ticket: self.bson_id_as_ref().cloned(),
})
.ok();
};
};
}
fn collapse_subtickets(&mut self, open: bool) {
if let Ok(subtickets) = self.ext.subtickets.as_mut() {
subtickets.iter_mut().for_each(|s| s.ext.open = open);
};
}
fn expand_unclosed_subtickets(&mut self) {
if let Ok(subtickets) = self.ext.subtickets.as_mut() {
subtickets.iter_mut().for_each(|s| s.open_mut_if_unclosed());
};
}
fn show_toggle_all_collapse_states_button(&mut self, ui: &mut egui::Ui) {
let text = RichText::new(if self.ext.batch_collapse {
"⏷ Expand All"
} else {
"➖ Collapse All"
});
if ui
.button(text)
.on_hover_text("Toggle all Subtickets' collapse states")
.clicked()
{
self.collapse_subtickets(self.ext.batch_collapse);
self.ext.batch_collapse = !self.ext.batch_collapse;
};
}
pub fn show_batch_action_buttons(&mut self, ui: &mut egui::Ui) {
if ui
.button("👁 All Unclosed")
.on_hover_text("Expand all unclosed Subtickets")
.clicked()
{
self.expand_unclosed_subtickets();
};
self.show_toggle_all_collapse_states_button(ui);
}
pub fn show_create_ticket_buttons(&mut self, ui: &mut egui::Ui, tx: &Sender<TicketAction>) {
ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
if ui
.button("🕱 Reset Drafts")
.on_hover_text(
RichText::new(
"Start over by removing all what are being written, including chosen images",
)
.color(Color32::RED),
)
.clicked()
{
self.ext.start_new_composing()
};
if ui.button("❌ Cancel").clicked() {
self.mode_mut(MediaMode::WriteSuggest);
};
if ui
.add_enabled(
self.is_draft_unwrap_not_empty(),
egui::Button::new(format!("✔ Create {} Feedback Ticket", self.type_as_str())),
)
.clicked()
{
tx.send(TicketAction::CreateTicket {
ticket: self.clone_with_composer(),
typ: self.typ.clone(),
}).ok();
};
});
});
}
}
#[derive(Debug, Clone)]
pub struct TicketExpand {
mode: MediaMode,
pub created_at_local: String,
pub tally: TicketTally,
viewing_locale: Locale,
pub subtickets: Result<Vec<SubTicket>, DatabaseError>,
pub subticket_draft: SubTicket,
ticket_draft: Option<Vec<SubTicketDiscardableCompose>>,
selected: bool,
batch_collapse: bool,
#[cfg(feature = "image_processing")]
images_loaded: bool,
}
impl Default for TicketExpand {
fn default() -> Self {
Self {
mode: Default::default(),
created_at_local: String::new(),
tally: Default::default(),
viewing_locale: Default::default(),
subtickets: Err(DatabaseError::Uninitialized),
subticket_draft: SubTicket::write_suggest(),
ticket_draft: None,
selected: false,
batch_collapse: false,
#[cfg(feature = "image_processing")]
images_loaded: false,
}
}
}
impl TicketExpand {
fn start_new_composing(&mut self) {
self.ticket_draft = Some(vec![SubTicketDiscardableCompose::first()]);
self.mode = MediaMode::WriteCompose;
}
fn selected_if_any_unclosed_subticket(mut self) -> Self {
if let Ok(subtickets) = self.subtickets.as_ref() {
if subtickets.iter().any(|subticket| !subticket.is_closed()) {
self.selected = true;
};
};
self
}
fn tally(mut self) -> Self {
self.tally =
TicketTally::from_subtickets(self.subtickets.as_ref().ok().map(|s| s.as_slice()));
self
}
}
#[cfg(feature = "gui")]
impl TicketExpand {
fn show_add_subticket_draft_unwrap_button(&mut self, ui: &mut egui::Ui) {
if ui
.button(RichText::new("➕ Add Subticket").strong())
.on_hover_text("Insert next subticket draft")
.clicked()
{
let len = self.ticket_draft.as_ref().unwrap().len();
self.ticket_draft
.as_mut()
.unwrap()
.push(SubTicketDiscardableCompose::next(len + 1));
}
}
fn discardable_composers_unwrap_ui(&mut self, ui: &mut egui::Ui, palette: &Palette) {
egui::ScrollArea::vertical()
.max_height(700.)
.show(ui, |ui| {
self.ticket_draft
.as_mut()
.unwrap()
.iter_mut()
.enumerate()
.filter(|(_, s)| !s.composer.is_none())
.for_each(|(i, s)| {
ui.allocate_space(egui::vec2(0., 10.));
s.unwrap_ui(ui, i, palette);
ui.separator();
});
});
}
}
fn local_time(utc: DateTime<Utc>, chrono_fmt: &str) -> String {
let local_time: DateTime<Local> = DateTime::from(utc);
format!("{}", local_time.format(chrono_fmt))
}
#[derive(Debug)]
pub struct TicketReadBuilder(Ticket);
impl TicketReadBuilder {
pub fn new(ticket: Ticket) -> Self {
Self(ticket)
}
pub fn finish(self, chrono_fmt: &str, preload_images: bool) -> Ticket {
let ticket = self
.0
.untitled_if_empty()
.selected_if_any_unclosed_subticket()
.subticket_draft_number()
.tally()
.convert_local_time(chrono_fmt);
if preload_images {
ticket.preload_tex_ids_if_selected()
} else {
ticket
}
}
}
#[derive(Debug)]
pub struct TicketWriteBuilder(Ticket);
impl TicketWriteBuilder {
pub fn new(ticket: Ticket) -> Self {
Self(ticket)
}
pub fn finish(self, sent_by: Staff, topical: &ProductionAsset) -> AnyResult<Ticket> {
Ok(self
.0
.topical_asset(topical)?
.sent_by(sent_by)
.created_now())
}
}