use super::*;
#[cfg(feature = "gui")]
use crossbeam_channel::Sender;
#[derive(Debug, Clone)]
pub enum TemplateAction {
Create(DeliveryTemplate),
Edit(DeliveryTemplate),
Delete(DeliveryTemplate),
MakeDefault(ProductionDefault),
SetAssetStage(StageIndex),
UnsetAssetStage(StageIndex),
}
#[async_trait]
pub trait DeliveryStages: DynClone + fmt::Debug + Send + Sync {
async fn delivery_template_from_bson_id(
&self,
project: &Project,
id: &ObjectId,
) -> Result<DeliveryTemplate, DatabaseError>;
async fn delivery_templates(
&self,
project: &Project,
) -> Result<Vec<DeliveryTemplate>, DatabaseError>;
async fn add_delivery_template(
&self,
project: &Project,
template: DeliveryTemplate,
) -> Result<(), ModificationError>;
async fn edit_delivery_template(
&self,
project: &Project,
existing: Option<DeliveryTemplate>,
updated: DeliveryTemplate,
) -> Result<(), ModificationError>;
async fn delete_delivery_template(
&self,
project: &Project,
template: DeliveryTemplate,
) -> Result<(), ModificationError>;
}
dyn_clone::clone_trait_object!(DeliveryStages);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeliveryTemplate {
#[serde(skip)]
mode: MediaMode,
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
id: Option<ObjectId>,
pub name: String,
pub description: String,
pub(super) stages: Vec<Option<Stage>>,
#[serde(skip)]
pub template: Template,
}
impl BsonId for DeliveryTemplate {
fn bson_id_as_ref(&self) -> Option<&ObjectId> {
self.id.as_ref()
}
fn bson_id(&self) -> AnyResult<&ObjectId> {
self.id
.as_ref()
.context("Bad DeliveryTemplate with no ObjectId")
}
}
impl DeliveryTemplate {
fn empty() -> Self {
Self {
mode: Default::default(),
id: None,
name: String::new(),
description: String::new(),
stages: vec![],
template: Template::empty(),
}
}
pub fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_owned(),
description: description.to_owned(),
template: Template::with_root(name, description),
..Self::empty()
}
}
#[cfg(debug_assertions)]
pub fn add_stage(&mut self, stage: Stage) {
self.stages.push(Some(stage));
}
fn empty_graph(&self) -> StageGraph {
StageGraph::with_root(&self.name, &self.description)
}
fn reset_graph(&mut self) {
self.template.graph = self.empty_graph();
}
pub(super) fn reset_reference(&mut self) {
self.template.stage_order.iter_mut().for_each(|s| {
s.has_reference_to = false;
});
}
pub fn stage_generic_purpose_mut(&mut self) {
self.stages.iter_mut().enumerate().for_each(|(i, s)| {
if let Some(stage) = s {
stage.index = Some(StageIndex::Generic(i as u32));
};
});
}
pub fn stage_internal_purpose_mut(&mut self) {
self.stages.iter_mut().enumerate().for_each(|(i, s)| {
if let Some(stage) = s {
stage.index = Some(StageIndex::Internal(i as u32));
};
});
}
pub fn stage_outgoing_purpose_mut(&mut self) {
self.stages.iter_mut().enumerate().for_each(|(i, s)| {
if let Some(stage) = s {
stage.index = Some(StageIndex::Outgoing(i as u32));
};
});
}
pub fn construct_stage_graph(&mut self) {
self.template.graph_mut_from_stages(&self.stages);
self.template.stage_order_mut();
}
pub fn rewrite_graph_from_stages(&mut self) {
self.stages.retain(|s| s.is_some());
self.reset_graph();
self.stages.iter_mut().for_each(|s| {
s.as_mut().unwrap().parse_dependency_draft();
});
let stages = self
.stages
.iter()
.map(|s| s.as_ref().unwrap())
.collect::<Vec<&Stage>>();
self.template.rewrite_graph_from_stages(stages.as_slice());
self.template.stage_order_mut();
}
fn name_check(&self) -> AnyResult<()> {
if self.name.is_empty() {
return Err(anyhow!("Workflow name cannot be empty"));
};
if self
.stages
.iter()
.filter(|s| s.is_some())
.any(|s| s.as_ref().unwrap().name.is_empty())
{
return Err(anyhow!("No stage name can be left blank"));
};
Ok(())
}
pub fn validate(&mut self) -> AnyResult<()> {
self.name_check()?;
self.rewrite_graph_from_stages();
match self
.template
.graph_err
.as_ref()
.unwrap()
.iter()
.filter(|r| r.is_err())
.next()
{
Some(r) => Err(anyhow!(
"The dependency graph is not valid: {}. Please fix the Stages and try again.",
r.as_ref().err().unwrap()
)),
None => Ok(()),
}
}
}
#[cfg(feature = "gui")]
impl DeliveryTemplate {
fn health_ui(&self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
self.template.graph_err_ui(ui);
ui.heading(&self.name);
});
}
fn validate_button(&mut self, ui: &mut egui::Ui) {
if ui
.button("🛃 Validate")
.on_hover_text("Check if the described dependency above is valid")
.clicked()
{
self.rewrite_graph_from_stages();
};
}
pub fn show_submit_draft_buttons(&mut self, ui: &mut egui::Ui, tx: &Sender<TemplateAction>) {
if let MediaMode::WriteCompose = self.mode {
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
if ui.button("❌ Cancel").clicked() {
self.mode_mut(MediaMode::WriteSuggest);
};
if ui
.button("✔ Create")
.on_hover_text("Submit this Workflow Definition for current project")
.clicked()
{
tx.send(TemplateAction::Create(self.clone())).ok();
};
self.validate_button(ui);
});
};
}
pub fn show_submit_edit_buttons(&mut self, ui: &mut egui::Ui, tx: &Sender<TemplateAction>) {
if let MediaMode::WriteCompose = self.mode {
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
if ui
.button("❌ Cancel")
.on_hover_text("Go back to View mode")
.clicked()
{
self.mode_mut(MediaMode::Read);
};
if ui
.button("✔ Submit")
.on_hover_text(format!("Save edits for {} definition", self.name))
.clicked()
{
tx.send(TemplateAction::Edit(self.clone())).ok();
};
self.validate_button(ui);
});
};
}
pub fn show_delete_button(&mut self, ui: &mut egui::Ui, tx: &Sender<TemplateAction>) {
if ui
.button(RichText::new("🗑").color(egui::Color32::RED))
.on_hover_text(format!("Delete definition of workflow \"{}\"", self.name))
.clicked()
{
tx.send(TemplateAction::Delete(self.clone())).ok();
};
}
pub fn show_edit_buttons(&mut self, ui: &mut egui::Ui) {
match &self.mode {
MediaMode::WriteCompose => {
}
_ => {
if ui
.button("🖊")
.on_hover_text(format!("Edit definition of workflow \"{}\"", self.name))
.clicked()
{
self.mode_mut(MediaMode::WriteCompose);
};
}
}
}
pub fn show_make_default_buttons(&mut self, ui: &mut egui::Ui, tx: &Sender<TemplateAction>) {
if ui
.button(RichText::new("O").strong())
.on_hover_text(
RichText::new(format!(
"Make \"{}\" workflow default for {}",
self.name,
ProductionDefault::outgoing_as_str()
))
.color(Color32::LIGHT_GREEN),
)
.clicked()
{
tx.send(TemplateAction::MakeDefault(ProductionDefault::Outgoing(
self.clone(),
)))
.ok();
};
if ui
.button(RichText::new("I").strong())
.on_hover_text(
RichText::new(format!(
"Make \"{}\" workflow default for {}",
self.name,
ProductionDefault::internal_as_str()
))
.color(Color32::LIGHT_RED),
)
.clicked()
{
tx.send(TemplateAction::MakeDefault(ProductionDefault::Internal(
self.clone(),
)))
.ok();
};
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
match &self.mode {
MediaMode::Read => self.read_mode_ui(ui),
MediaMode::WriteSuggest => {
self.write_suggest_ui(ui);
}
MediaMode::WriteCompose => {
self.write_compose_ui(ui);
}
MediaMode::WriteEdit => {
self.write_edit_ui(ui);
}
}
}
}
impl ReadWriteSuggest for DeliveryTemplate {
fn write_suggest() -> Self {
let mut draft = Self {
mode: MediaMode::WriteSuggest,
..Self::empty()
};
draft.template.graph_err = Some(vec![Err(GraphError::NotChecked)]);
draft
}
fn with_mode(mut self, mode: MediaMode) -> Self {
self.mode_mut(mode);
self
}
fn mode(&self) -> &MediaMode {
&self.mode
}
fn mode_mut(&mut self, mode: MediaMode) {
let should_init_draft = mode == MediaMode::WriteCompose;
self.stages.iter_mut().for_each(|s| {
if let Some(stage) = s {
stage.mode_mut(mode.clone());
if should_init_draft {
stage.init_draft_from_existing();
};
};
});
self.mode = mode;
}
#[cfg(feature = "gui")]
fn read_mode_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| {
self.health_ui(ui);
ui.label(&self.description);
self.template.stage_order_vertical_ui(ui, &self.name);
});
}
#[cfg(feature = "gui")]
fn write_suggest_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
if ui.button("➕ New Workflow Definition").clicked() {
self.mode_mut(MediaMode::WriteCompose);
};
});
}
#[cfg(feature = "gui")]
fn write_compose_ui(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| {
ui.horizontal(|ui| {
self.template.graph_err_ui(ui);
ui.label("Workflow name:");
ui.add(egui::TextEdit::singleline(&mut self.name).desired_width(150.));
});
ui.horizontal(|ui| {
ui.label("Description:");
ui.text_edit_singleline(&mut self.description);
});
egui::ScrollArea::vertical()
.id_source(&self.name)
.min_scrolled_height(500.)
.max_height(900.)
.show(ui, |ui| {
self.stages
.iter_mut()
.filter(|s| s.is_some())
.for_each(|s| {
ui.group(|ui| {
s.as_mut().unwrap().ui(ui);
if ui
.button("🗑 Remove")
.on_hover_text(
if self.name.is_empty() {
RichText::new("Delete this Stage")
} else {
RichText::new(format!("Delete \"{}\" stage", self.name))
}
.color(Color32::RED),
)
.clicked()
{
s.take();
};
});
});
});
ui.horizontal(|ui| {
if ui
.button(RichText::new("➕ Add Stage").strong())
.on_hover_text("Add one more Stage to this Workflow")
.clicked()
{
self.stages.push(Some(Stage::draft()));
};
});
});
}
}
#[derive(Debug)]
pub struct DeliTemplateReadBuilder(DeliveryTemplate);
impl DeliTemplateReadBuilder {
pub fn new(template: DeliveryTemplate) -> Self {
Self(template)
}
pub fn internal_stages_only(mut self) -> Self {
self.0.template.stage_order = self
.0
.template
.stage_order
.into_iter()
.filter(|s| s.is_internal)
.collect();
self
}
pub fn outgoing_stages_only(mut self) -> Self {
self.0.template.stage_order = self
.0
.template
.stage_order
.into_iter()
.filter(|s| !s.is_internal)
.collect();
self
}
pub fn construct_generic_stage_graph(mut self) -> Self {
self.0.reset_graph();
self.0.stage_generic_purpose_mut();
self.0.construct_stage_graph();
self
}
pub fn construct_internal_stage_graph(mut self) -> Self {
self.0.reset_graph();
self.0.stage_internal_purpose_mut();
self.0.construct_stage_graph();
self
}
pub fn construct_outgoing_stage_graph(mut self) -> Self {
self.0.reset_graph();
self.0.stage_outgoing_purpose_mut();
self.0.construct_stage_graph();
self
}
pub fn finish(self) -> DeliveryTemplate {
self.0
}
}
pub struct DeliTemplateWriteBuilder(DeliveryTemplate);
impl DeliTemplateWriteBuilder {
pub fn new(template: DeliveryTemplate) -> Self {
Self(template)
}
pub fn finish(mut self) -> DeliveryTemplate {
self.0.name = self.0.name.trim().to_owned();
self.0.description = self.0.description.trim().to_owned();
self.0
}
}