hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
mod subticket;
mod subticket_content;
mod ticket;

pub use subticket::*;
pub use subticket_content::*;
pub use ticket::*;

use super::*;
use crate::media::*;
use crate::prelude::ticket_tally::TicketTally;
use hconf::palette::{Palette, Rgb};
use mkutil::dialog;

use chrono::prelude::{DateTime, Utc};
use crossbeam_channel::Sender;

pub const MODIFIER_LAZY_HINT: &str =
    "Hold Alt and click for a lazy send (delaying full UI refresh)";

#[derive(Debug, Clone)]
/// All actions related to `DefectTracking` system:
///   - Tickets
///   - Subtickets
///   - Statuses
///   - Replies
pub enum TicketAction {
    CreateTicket {
        ticket: Ticket,
        typ: TicketAuthorSource,
    },

    DeleteTicket {
        ticket: Option<ObjectId>,
    },

    // SubTicket
    AddSubTicket {
        ticket: Option<ObjectId>,
        typ: TicketAuthorSource,
        subticket: SubTicket,
        tally: TicketTally,
    },

    SetSubTicketStatus {
        subticket_id: Option<ObjectId>,
        subticket_number: u8,
        typ: TicketAuthorSource,
        status: SubTicketStatus,
        tally: TicketTally,
        lazy: bool,
    },

    EditSubTicket {
        existing: Option<ObjectId>,
        updated: SubTicket,
    },

    DeleteSubTicket {
        ticket: Option<ObjectId>,
        subticket: Option<ObjectId>,
    },

    // Reply
    AddReply {
        subticket: Option<ObjectId>,
        reply: Reply,
        lazy: bool,
    },

    EditReply {
        subticket: Option<ObjectId>,
        existing: Option<Reply>,
        updated: Reply,
        lazy: bool,
    },

    DeleteReply {
        subticket: Option<ObjectId>,
        reply: Reply,
        lazy: bool,
    },
}

#[async_trait]
/// Interface to how CRUDs for tickets (and subtickets) -- in "defect tracking" -- are handled.
pub trait TicketTracking: DynClone + fmt::Debug + Send + Sync {
    /// Adds a new `Ticket` with no `SubTicket`s children.
    async fn add_container_ticket(
        &self,
        project: &Project,
        ticket: BloatedTicket,
    ) -> Result<ObjectId, ModificationError>;

    /// Adds all `SubTicket`s children to the given `Ticket` parent.
    async fn add_subtickets_to_container(
        &self,
        project: &Project,
        ticket: &ObjectId,
        subtickets: &[SubTicket],
    ) -> Result<TicketTally, ModificationError>;

    /// Gets all tickets of the given ProductionAsset. "Expanded" meaning their `SubTicket` children
    /// are fully processed for reading.
    async fn expanded_tickets(
        &self,
        project: &Project,
        asset: &ProductionAsset,
        order: &CreatedAtOrdering,
        chrono_fmt: &ChronoFormat,
        current_user: &Staff,
        layout: &EmbedLayoutPref,
        preload_images: bool,
    ) -> Result<Vec<Ticket>, DatabaseError>;

    /// Gets all subitems of the given Ticket.
    async fn subtickets(
        &self,
        project: &Project,
        ticket: &Ticket,
    ) -> Result<Vec<SubTicket>, DatabaseError>;

    /// Deletes the given ticket.
    async fn delete_ticket(
        &self,
        project: &Project,
        ticket: &ObjectId,
    ) -> Result<(), ModificationError>;

    /// Adds a new subticket and returns its `ObjectId`.
    async fn add_subticket(
        &self,
        project: &Project,
        ticket: &ObjectId,
        subticket: &SubTicket,
    ) -> Result<ObjectId, ModificationError>;

    /// Updates an existing subticket.
    async fn edit_subticket(
        &self,
        project: &Project,
        existing: Option<&SubTicket>,
        updated: &SubTicket,
    ) -> Result<(), ModificationError>;

    /// Deletes the given subticket.
    async fn delete_subticket(
        &self,
        project: &Project,
        ticket: Option<&ObjectId>,
        subticket: &ObjectId,
    ) -> Result<(), ModificationError>;

    /// Adds a new reply into reply thread of the given subitem.
    /// Sets status for the given subitem and logs who submits the change.
    async fn set_subticket_status(
        &self,
        project: &Project,
        subticket: &ObjectId,
        status: &SubTicketStatus,
        modified_by: &Staff,
    ) -> Result<(), ModificationError>;

    async fn add_reply(
        &self,
        project: &Project,
        subticket: &ObjectId,
        reply: &Reply,
    ) -> Result<(), ModificationError>;

    async fn edit_reply(
        &self,
        project: &Project,
        subticket: &ObjectId,
        existing: Option<&Reply>,
        updated: &Reply,
    ) -> Result<(), ModificationError>;

    /// Deletes a reply from reply thread of the given subitem.
    async fn delete_reply(
        &self,
        project: &Project,
        subticket: &ObjectId,
        reply: &Reply,
    ) -> Result<(), ModificationError>;
}

dyn_clone::clone_trait_object!(TicketTracking);

// ----------------------------------------------------------------------------
#[derive(
    Deserialize_repr,
    Serialize_repr,
    Debug,
    Clone,
    Default,
    PartialEq,
    Eq,
    strum::AsRefStr,
    strum::EnumIter,
)]
#[repr(u8)]
/// The agent where the feedback comes from.
/// LEGACY DESIGN: DO NOT change `repr` values.
pub enum TicketAuthorSource {
    // 📢 🔊 💥 💡
    #[default]
    #[strum(serialize = "📣 AD")]
    ArtDirector = 0,

    #[strum(serialize = "🚔 Client")]
    Client = 1,

    #[strum(serialize = "🔔 TeamLead")]
    TeamLead = 2,
}

#[cfg(feature = "gui")]
impl TicketAuthorSource {
    pub fn color32(&self, palette: &Palette) -> Color32 {
        match self {
            Self::ArtDirector => AssetStatus::AdFeedback.color32(palette),
            Self::Client => AssetStatus::ClientFeedback.color32(palette),
            Self::TeamLead => Color32::LIGHT_BLUE,
        }
    }

    pub fn colored_label(&self, palette: &Palette) -> RichText {
        RichText::new(self.as_ref())
            .color(Color32::BLACK)
            .background_color(self.color32(palette))
    }
}

#[cfg(feature = "gui")]
/// Ticket source option radio buttons with simple mut argument.
pub fn ticket_author_source_options_ui(ui: &mut egui::Ui, source: &mut TicketAuthorSource) {
    ui.horizontal(|ui| {
        ui.label("Agent:");
        for src in TicketAuthorSource::iter() {
            ui.selectable_value(source, src.clone(), src.as_ref());
        }
    });
}

// ----------------------------------------------------------------------------
#[derive(
    Deserialize_repr,
    Serialize_repr,
    Debug,
    Clone,
    Default,
    PartialEq,
    Eq,
    strum::AsRefStr,
    strum::EnumIter,
)]
#[repr(u8)]
/// Types a [`SubTicket`] can be.
pub enum SubTicketClass {
    #[default]
    #[strum(serialize = "🚧 Defect")]
    Defect = 0,

    #[strum(serialize = "🔀 Direction")]
    Direction = 1,
}

#[cfg(feature = "gui")]
impl SubTicketClass {
    fn color_rgb<'a>(&self, palette: &'a Palette) -> &'a Rgb {
        let p = &palette.dark;
        match self {
            Self::Defect => &p.rgb.SUBTICKET_DEFECT,
            Self::Direction => &p.rgb.SUBTICKET_DIRECTION,
        }
    }

    pub fn color32(&self, palette: &Palette) -> Color32 {
        let color = self.color_rgb(palette);
        Color32::from_rgb(color.r, color.g, color.b)
    }

    pub fn colored_label(&self, palette: &Palette) -> RichText {
        RichText::new(self.as_ref())
            .color(Color32::BLACK)
            .background_color(self.color32(palette))
    }
}

#[cfg(feature = "gui")]
/// SubTicket class option radio buttons with simple mut argument.
pub fn subticket_class_options_ui(
    ui: &mut egui::Ui,
    class: &mut SubTicketClass,
    palette: &Palette,
) {
    ui.horizontal(|ui| {
        ui.label("Classification:");
        for cls in SubTicketClass::iter() {
            let text = if *class == cls {
                cls.colored_label(palette)
            } else {
                RichText::new(cls.as_ref())
            };

            ui.radio_value(class, cls, text);
        }
    });
}

// ----------------------------------------------------------------------------
#[derive(Debug, Clone, Default)]
struct SubTicketDiscardableCompose {
    subject: String,
    class: SubTicketClass,
    composer: DiscardableComposer,
}

impl SubTicketDiscardableCompose {
    fn first() -> Self {
        Self {
            composer: DiscardableComposer::new("Write subticket #1:"),
            ..Default::default()
        }
    }

    fn next(number: usize) -> Self {
        Self {
            composer: DiscardableComposer::new(&format!("Write subticket #{}:", number)),
            ..Default::default()
        }
    }

    #[cfg(feature = "gui")]
    fn unwrap_ui(&mut self, ui: &mut egui::Ui, idx: usize, palette: &Palette) {
        // `SubTicket` class
        subticket_class_options_ui(ui, &mut self.class, palette);

        // composing `SubTicket` subject
        ui.horizontal(|ui| {
            ui.heading("Subject:");
            ui.add(egui::TextEdit::singleline(&mut self.subject).desired_width(f32::INFINITY));
        });

        // composing `SubTicket` content
        self.composer.draft_unwrap_with_discard_ui(ui, idx);
    }
}

// ----------------------------------------------------------------------------
#[derive(Debug, Clone, Default)]
pub struct SubTicketCompose {
    subject: String,
    class: SubTicketClass,
    composer: Composer,
}

impl SubTicketCompose {
    pub fn upload_images(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        upload: impl Fn(&Path, &Project, &ProductionAsset) -> AnyResult<PathBuf>,
    ) -> ImageUploaded {
        self.composer.upload_images(project, asset, upload)
    }
}

impl TryFrom<SubTicketDiscardableCompose> for SubTicketCompose {
    type Error = &'static str;

    fn try_from(discardable: SubTicketDiscardableCompose) -> Result<Self, Self::Error> {
        match discardable.composer.0 {
            Some(composer) => Ok(Self {
                subject: discardable.subject,
                class: discardable.class,
                composer,
            }),
            None => Err("Composer was discarded by user"),
        }
    }
}

// ----------------------------------------------------------------------------
#[derive(Debug, Serialize)]
/// With additional fields which are required by legacy Python clients,
/// since historically `Defect Tracking` was an extension from the `Query Message` system.
pub struct BloatedTicket {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    id: Option<ObjectId>,
    soup_format: MsgTopic,
    project: String,
    sender: String,
    subject: String,
    content: Vec<ObjectId>,
    topical_assets: Vec<ObjectId>,
    recipients: Vec<String>,
    seen_by: Vec<String>,
    source_type: TicketAuthorSource,
    orig_locale: Locale,
    #[cfg_attr(
        feature = "mongo",
        serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")
    )]
    datetime: DateTime<Utc>,
    instantiated_for: Vec<String>,
    /// IMPORTANT: This won't be used, but it is required by legacy Python client.
    as_reply_to: Option<Vec<ObjectId>>,
    /// IMPORTANT: This won't be used, but it is required by legacy Python client.
    hyperlinks: Option<Vec<HtmlLink>>,
    /// IMPORTANT: This won't be used, but it is required by legacy Python client.
    is_plain_text: bool,
    /// IMPORTANT: This won't be used, but it is required by legacy Python client.
    is_important: bool,
    /// IMPORTANT: This won't be used, but it is required by legacy Python client.
    is_urgent: bool,
}

#[derive(Debug)]
pub struct BloatedTicketBuilder(Ticket);

impl BloatedTicketBuilder {
    pub fn new(ticket: Ticket) -> Self {
        Self(ticket)
    }

    pub fn finish(
        self,
        project: &Project,
        assignees: &[Staff],
        role_map: &RoleMap,
    ) -> AnyResult<BloatedTicket> {
        let recipients: Vec<String> = self
            .0
            .topic
            .receipt_recipients(assignees, role_map)?
            .iter()
            .map(|s| s.name_unwrap().to_owned())
            .collect();

        Ok(BloatedTicket {
            id: self.0.id,
            soup_format: self.0.topic,
            project: project.as_str().to_owned(),
            sender: self.0.sender,
            subject: self.0.name,
            content: self.0.content,
            topical_assets: self.0.topical_assets,
            recipients,
            seen_by: vec![],
            source_type: self.0.typ,
            orig_locale: self.0.composed_with_locale,
            datetime: self.0.created_at,
            instantiated_for: self.0.instantiated_for,
            as_reply_to: None,
            hyperlinks: None,
            is_plain_text: false, // NOTE: this MUST be `false`
            is_important: false,
            is_urgent: false,
        })
    }
}