use super::*;
use crate::media::Reply;
use mkutil::html_scraping::PlaceImageHint;
const SUBJECT_MAX_LEN: usize = 40;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "mongo", derive(Serialize, Deserialize))]
pub struct SubTicket {
#[cfg_attr(feature = "mongo", serde(skip))]
pub ext: SubTicketExpand,
#[cfg_attr(
feature = "mongo",
serde(rename = "_id", skip_serializing_if = "Option::is_none")
)]
pub id: Option<ObjectId>,
pub number: u8,
#[cfg_attr(feature = "mongo", serde(skip_serializing_if = "Option::is_none"))]
subject: Option<String>,
#[cfg_attr(feature = "mongo", serde(default))]
pub class: SubTicketClass,
#[cfg_attr(feature = "mongo", serde(rename = "feedback_notes"))]
raw_note: SubTicketRawNote,
#[cfg_attr(feature = "mongo", serde(rename = "status"))]
raw_status: SubTicketRawStatus,
#[cfg_attr(
feature = "mongo",
serde(rename = "responses", skip_serializing_if = "Option::is_none")
)]
pub discussion: Option<Vec<Reply>>,
}
impl BsonId for SubTicket {
fn bson_id_as_ref(&self) -> Option<&ObjectId> {
self.id.as_ref()
}
fn bson_id(&self) -> AnyResult<&ObjectId> {
self.id.as_ref().context("SubTicket without BSON ObjectId")
}
}
impl ReadWriteSuggest for SubTicket {
fn write_suggest() -> Self {
Self::empty().with_mode(MediaMode::WriteSuggest)
}
fn with_mode(mut self, mode: MediaMode) -> Self {
self.mode_mut(mode);
self
}
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) {
ui.colored_label(Color32::TEMPORARY_COLOR, "Raw Notes")
.on_hover_text(format!("{:?}", self.raw_note));
}
#[cfg(feature = "gui")]
fn write_suggest_ui(&mut self, ui: &mut egui::Ui) {
if ui
.button(format!("➕ Add Subticket #{}", self.number))
.on_hover_text("Add one more subticket into this ticket")
.clicked()
{
if self.ext.composer.is_none() {
self.subject = Some(String::new());
self.ext.start_new_composing();
} else {
self.mode_mut(MediaMode::WriteCompose);
};
};
}
}
impl SubTicket {
pub fn empty() -> Self {
Self {
ext: Default::default(),
id: None,
number: 0,
subject: None,
class: Default::default(),
raw_note: SubTicketRawNote::empty(),
raw_status: SubTicketRawStatus::empty(),
discussion: None,
}
}
pub fn with_subject(subject: String) -> Self {
Self {
subject: if subject.is_empty() {
None
} else {
Some(subject)
},
..Self::empty()
}
}
pub(super) fn number(mut self, number: u8) -> Self {
self.number = number;
self
}
fn set_open(mut self, modified_by: &str) -> Self {
self.raw_status = SubTicketRawStatus::as_open(modified_by);
self
}
fn note(mut self, note: SubTicketRawNote) -> Self {
self.raw_note = note;
self
}
fn class(mut self, class: SubTicketClass) -> Self {
self.class = class;
self
}
fn convert_status(mut self) -> Self {
self.ext.status = (&self.raw_status).into();
self
}
fn status_label(mut self) -> Self {
self.ext.status_label = match &self.subject {
Some(title) => {
let mut title_excerpt = title.to_owned();
if let Ordering::Greater = title.len().cmp(&SUBJECT_MAX_LEN) {
title_excerpt.truncate(SUBJECT_MAX_LEN);
title_excerpt.push_str("...");
};
format!(
"Subticket {} ({}): {}",
self.number,
self.ext.status.as_ref(),
title_excerpt
)
}
None => format!("Subticket {}: {}", self.number, self.ext.status.as_ref()),
};
self
}
fn note_with_image_array(mut self, img_hint: &PlaceImageHint) -> Self {
self.ext.multilingual_note = MultilingualNote::to_image_array(&self.raw_note, img_hint);
self
}
#[cfg(feature = "gui")]
fn note_with_inplace_embed(mut self, img_hint: &PlaceImageHint) -> Self {
self.ext.multilingual_note = MultilingualNote::to_inplace_embed(&self.raw_note, img_hint);
self
}
#[cfg(all(feature = "html", feature = "gui"))]
fn with_discussion(
mut self,
current_username: &str,
chrono_fmt: &str,
layout: &EmbedLayoutPref,
img_hint: &PlaceImageHint,
) -> Self {
let discussion = self.discussion.take().map(|replies| {
replies
.into_iter()
.map(|r| {
ReplyReadBuilder::new(r).finish(current_username, chrono_fmt, layout, img_hint)
})
.collect()
});
self.discussion = discussion;
self.ext.reply_draft.mode_mut(MediaMode::WriteSuggest);
self
}
fn expand(mut self, ext: SubTicketExpand) -> Self {
self.ext = ext;
self
}
fn from_draft(
draft: SubTicketCompose,
composing_locale: &Locale,
) -> AnyResult<Self> {
Ok(SubTicket::with_subject(draft.subject)
.note(
draft
.composer
.into_subticket_multilingual_note(composing_locale)?,
)
.class(draft.class))
}
pub fn from_composers(
composers: Vec<SubTicketCompose>,
composing_locale: &Locale,
) -> Vec<Self> {
composers
.into_iter()
.filter_map(|draft| Self::from_draft(draft, composing_locale).ok())
.enumerate()
.map(|(i, s)| s.number((i + 1) as u8))
.collect()
}
fn note_from_composer(mut self) -> AnyResult<Self> {
self.raw_note = self
.ext
.composer
.inner()?
.into_subticket_multilingual_note(&self.ext.composing_locale)?;
Ok(self)
}
}
impl SubTicket {
#[cfg(feature = "gui")]
pub fn status_action(
&self,
ui: &mut egui::Ui,
typ: &TicketAuthorSource,
tally: &TicketTally,
status: SubTicketStatus,
) -> TicketAction {
TicketAction::SetSubTicketStatus {
subticket_id: self.id.clone(),
subticket_number: self.number,
typ: typ.to_owned(),
status,
tally: tally.to_owned(),
lazy: ui.ctx().input(|i| i.modifiers.alt),
}
}
pub(super) fn open_mut_if_unclosed(&mut self) {
self.ext.open = self.is_unclosed();
}
fn clone_with_composer(&mut self) -> Self {
let ext = std::mem::take(&mut self.ext);
self.clone().expand(ext)
}
pub fn toggle_collapse_state(&mut self) {
self.ext.open = !self.ext.open;
}
pub fn status(&self) -> &SubTicketStatus {
&self.ext.status
}
pub(crate) fn is_closed(&self) -> bool {
self.ext.status == SubTicketStatus::Closed
}
pub fn is_strictly_addressed(&self) -> bool {
!self.raw_status.is_literal_closed() && !self.raw_status.is_literal_open()
}
pub(crate) fn is_addressed(&self) -> bool {
matches!(&self.ext.status, SubTicketStatus::Addressed)
}
fn is_unclosed(&self) -> bool {
matches!(
&self.ext.status,
SubTicketStatus::Addressed | SubTicketStatus::Open
)
}
pub fn composer_is_none(&self) -> bool {
self.ext.composer.is_none()
}
pub fn composer_as_mut_unwrap(&mut self) -> &mut Composer {
self.ext.composer.inner_as_mut_unwrap()
}
pub(super) fn is_composer_unwrap_empty(&self) -> bool {
self.ext.composer.inner_as_ref_unwrap().is_empty()
}
pub fn upload_images(
&mut self,
project: &Project,
asset: &ProductionAsset,
upload: impl Fn(&Path, &Project, &ProductionAsset) -> AnyResult<PathBuf>,
) -> ImageUploaded {
self.ext
.composer
.inner_as_mut_unwrap()
.upload_images(project, asset, upload)
}
pub(super) fn texture_ids_mut(&mut self) {
self.ext.texture_ids_mut();
if let Some(discussion) = self.discussion.as_mut() {
discussion.iter_mut().for_each(|reply| {
reply.texture_ids_mut();
});
};
}
pub fn reply_box_width_mut(&mut self, width: f32) {
self.ext.reply_box_max_width = width;
}
pub(super) fn viewing_locale_mut(&mut self, locale: &Locale) {
self.ext.viewing_locale = locale.clone();
}
}
#[cfg(feature = "gui")]
impl SubTicket {
pub fn status_colored_text(&self, palette: &Palette) -> RichText {
RichText::new(&self.ext.status_label)
.heading()
.strong()
.color(self.ext.status.color32(palette))
}
fn subject_mut_unwrap_ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.heading("Subject:");
ui.add(
egui::TextEdit::singleline(self.subject.as_mut().unwrap())
.desired_width(f32::INFINITY),
);
});
}
pub fn edit_metadata_unwrap_ui(&mut self, ui: &mut egui::Ui, palette: &Palette) {
locale::locale_options_ui(ui, &mut self.ext.composing_locale);
subticket_class_options_ui(ui, &mut self.class, palette);
self.subject_mut_unwrap_ui(ui);
}
pub fn show_mark_close_button(
&self,
ui: &mut egui::Ui,
authorized: bool,
typ: &TicketAuthorSource,
tally: &TicketTally,
tx: &Sender<TicketAction>,
) {
if ui
.add_enabled(
authorized,
egui::Button::new(format!("✅ Mark #{} as Closed", self.number)),
)
.on_hover_text(MODIFIER_LAZY_HINT)
.clicked()
{
tx.send(self.status_action(ui, typ, tally, SubTicketStatus::Closed))
.ok();
};
}
pub fn show_mark_addressed_button(
&self,
ui: &mut egui::Ui,
authorized: bool,
typ: &TicketAuthorSource,
tally: &TicketTally,
tx: &Sender<TicketAction>,
) {
if ui
.add_enabled(
authorized,
egui::Button::new(format!("🙋 Mark #{} as Addressed", self.number)),
)
.on_hover_text(MODIFIER_LAZY_HINT)
.clicked()
{
tx.send(self.status_action(ui, typ, tally, SubTicketStatus::Addressed))
.ok();
};
}
pub fn show_mark_open_button(
&self,
ui: &mut egui::Ui,
authorized: bool,
typ: &TicketAuthorSource,
tally: &TicketTally,
tx: &Sender<TicketAction>,
) {
if ui
.add_enabled(authorized, self.ext.status.mark_open_button(&self.number))
.on_hover_text(MODIFIER_LAZY_HINT)
.clicked()
{
tx.send(self.status_action(ui, typ, tally, SubTicketStatus::Open))
.ok();
};
}
pub fn show_submit_draft_unwrap_buttons(
&mut self,
ui: &mut egui::Ui,
ticket: Option<&ObjectId>,
typ: &TicketAuthorSource,
tally: &TicketTally,
tx: &Sender<TicketAction>,
) {
ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
if ui.button("❌ Cancel").clicked() {
self.mode_mut(MediaMode::WriteSuggest);
};
if ui
.add_enabled(
!self.is_composer_unwrap_empty(),
egui::Button::new("✔ Send"),
)
.clicked()
{
tx.send(TicketAction::AddSubTicket {
ticket: ticket.cloned(),
typ: typ.to_owned(),
tally: tally.to_owned(),
subticket: self.clone_with_composer(),
})
.ok();
}
});
});
}
pub fn show_submit_edit_unwrap_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("❌ Cancel").clicked() {
self.mode_mut(MediaMode::Read);
};
if ui
.add_enabled(
!self.ext.composer.0.as_ref().unwrap().is_empty(),
egui::Button::new("✔ Submit Edits"),
)
.clicked()
{
if dialog::confirm_dialog(
"Are you sure?",
"Existing Subticket will be overwritten",
) {
tx.send(TicketAction::EditSubTicket {
existing: None,
updated: self.clone_with_composer(),
})
.ok();
};
}
});
});
}
pub fn show_edit_button(&mut self, ui: &mut egui::Ui, orig_locale: &Locale) {
if ui
.button("🖊")
.on_hover_text("Edit this Subticket")
.clicked()
{
if self.subject.is_none() {
self.subject = Some(String::new());
};
self.ext.start_editing(orig_locale);
};
}
pub fn show_delete_button(
&self,
ui: &mut egui::Ui,
ticket: Option<&ObjectId>,
tx: &Sender<TicketAction>,
) {
if ui
.add(egui::Button::new("🗑"))
.on_hover_text(
RichText::new(format!("Delete Subticket #{}", self.number)).color(Color32::RED),
)
.clicked()
{
if dialog::confirm_dialog(
&format!("Delete Subticket #{}?", self.number),
"Deleting subticket cannot be undone",
) {
tx.send(TicketAction::DeleteSubTicket {
ticket: ticket.cloned(),
subticket: self.id.clone(),
})
.ok();
};
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SubTicketExpand {
mode: MediaMode,
pub open: bool,
status: SubTicketStatus,
status_label: String,
multilingual_note: MultilingualNote,
viewing_locale: Locale,
composer: DiscardableComposer,
composing_locale: Locale,
pub reply_draft: Reply,
pub reply_box_max_width: f32,
}
impl SubTicketExpand {
fn start_new_composing(&mut self) {
self.composer = DiscardableComposer::new("Write your new subticket:");
self.mode = MediaMode::WriteCompose;
}
fn start_editing(&mut self, orig_locale: &Locale) {
if let Some(note) = self.multilingual_note.localized_note_as_ref(orig_locale) {
self.composer = DiscardableComposer::from_note(note, "Edit feedback note:");
self.mode = MediaMode::WriteEdit;
};
}
pub fn note_as_mut(&mut self) -> Option<&mut Note> {
self.multilingual_note
.localized_note_as_mut(&self.viewing_locale)
}
pub fn discardable_composer_as_mut_unwrap(&mut self) -> &mut Composer {
self.composer.inner_as_mut_unwrap()
}
fn texture_ids_mut(&mut self) {
self.multilingual_note.texture_ids_mut();
}
}
#[cfg(feature = "gui")]
#[derive(Debug)]
pub(super) struct SubTicketReadBuilder(SubTicket);
#[cfg(feature = "gui")]
impl SubTicketReadBuilder {
pub(super) fn new(subticket: SubTicket) -> Self {
Self(subticket)
}
pub(super) fn finish(
self,
current_username: &str,
chrono_fmt: &str,
layout: &EmbedLayoutPref,
img_hint: &PlaceImageHint,
) -> SubTicket {
match layout {
EmbedLayoutPref::RowOrColumn => self.0.note_with_image_array(img_hint),
EmbedLayoutPref::Carousel => self.0.note_with_inplace_embed(img_hint),
}
.convert_status()
.status_label()
.with_discussion(current_username, chrono_fmt, layout, img_hint)
}
}
#[derive(Debug)]
pub struct SubTicketWriteBuilder(SubTicket);
impl SubTicketWriteBuilder {
pub fn new(subticket: SubTicket) -> Self {
Self(subticket)
}
pub fn finish(self, modified_by: Staff) -> AnyResult<SubTicket> {
Ok(self
.0
.set_open(&modified_by.name_unwrap())
.note_from_composer()?)
}
}