use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use eframe::egui;
use crate::family_table::{FamilyTable, GenerateReport};
use crate::plm::client::PlmClient;
use crate::plm::family::{self, Attribute};
use crate::plm::PlmFuture;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Info {
pub number: String,
pub member_part_type: String,
pub member_part_type_mode: String,
pub category: String,
pub schema: Vec<Attribute>,
}
pub(crate) struct Pending<T> {
future: PlmFuture<T>,
}
impl<T> Pending<T> {
pub(crate) fn new(future: PlmFuture<T>) -> Self {
Self { future }
}
pub(crate) fn poll(&mut self) -> Option<Result<T, String>> {
let mut cx = Context::from_waker(Waker::noop());
match self.future.as_mut().poll(&mut cx) {
Poll::Ready(result) => Some(result),
Poll::Pending => None,
}
}
}
#[derive(Default)]
pub struct PlmFamily {
client: Option<Rc<PlmClient>>,
key: Option<String>,
info: Option<Info>,
info_task: Option<Pending<Info>>,
generate_task: Option<Pending<GenerateReport>>,
set_type_task: Option<Pending<String>>,
pub refused_unsaved: bool,
save_request: bool,
generate_after_save: bool,
pub member_type_input: String,
pub message: Option<String>,
}
impl PlmFamily {
pub fn sync(&mut self, client: Option<Rc<PlmClient>>, key: Option<&str>) {
let key = if client.is_some() { key.and_then(family::revision_key) } else { None };
let client = client.filter(|_| key.is_some());
let same = self.key == key && self.client.as_ref().map(Rc::as_ptr) == client.as_ref().map(Rc::as_ptr);
if same {
return;
}
*self = Self { client, key, ..Self::default() };
self.reload();
}
pub fn active(&self) -> bool {
self.client.is_some()
}
pub fn info(&self) -> Option<&Info> {
self.info.as_ref()
}
pub fn busy(&self) -> bool {
self.info_task.is_some() || self.generate_task.is_some() || self.set_type_task.is_some() || self.generate_after_save
}
pub fn generating(&self) -> bool {
self.generate_task.is_some() || self.generate_after_save
}
fn reload(&mut self) {
let (Some(client), Some(key)) = (self.client.clone(), self.key.clone()) else { return };
self.info_task = Some(Pending::new(Box::pin(async move { load_info(&client, &key).await })));
}
pub fn request_generate(&mut self, dirty: bool, json: String) {
let (Some(client), Some(key)) = (self.client.clone(), self.key.clone()) else { return };
if self.generating() {
return;
}
if dirty {
self.refused_unsaved = true;
self.message = Some(family::UNSAVED_FAMILY.into());
return;
}
self.refused_unsaved = false;
self.message = Some("generating on the server…".into());
self.generate_task = Some(Pending::new(Box::pin(async move {
family::generate(&client, &key, &json).await.map_err(|e| e.to_string())
})));
}
pub fn request_save_and_generate(&mut self) {
if self.active() && !self.generating() {
self.refused_unsaved = false;
self.save_request = true;
self.message = Some("saving the family…".into());
}
}
pub fn take_save_request(&mut self) -> bool {
let asked = std::mem::take(&mut self.save_request);
if asked {
self.generate_after_save = true;
}
asked
}
pub fn save_failed(&mut self, error: String) {
self.generate_after_save = false;
self.message = Some(format!("the family was not saved, so nothing was generated: {error}"));
}
pub fn request_member_type(&mut self) {
let (Some(client), Some(key)) = (self.client.clone(), self.key.clone()) else { return };
let Some((part, _)) = family::key_ids(&key).map(|(p, r)| (p.to_string(), r.to_string())) else { return };
let wanted = self.member_type_input.trim().to_string();
self.set_type_task = Some(Pending::new(Box::pin(async move {
family::set_member_part_type(&client, &part, &wanted).await.map_err(|e| e.to_string())?;
Ok(wanted)
})));
}
pub fn poll(&mut self, dirty: bool, writes_pending: usize, json: impl FnOnce() -> String) -> Option<GenerateReport> {
if let Some(task) = self.info_task.as_mut() {
if let Some(result) = task.poll() {
self.info_task = None;
match result {
Ok(info) => {
let untouched = self.member_type_input.trim().is_empty()
|| self.info.as_ref().is_some_and(|old| old.member_part_type == self.member_type_input.trim());
if untouched {
self.member_type_input = info.member_part_type.clone();
}
self.info = Some(info);
}
Err(error) => self.message = Some(format!("the family's server details could not be read: {error}")),
}
}
}
if let Some(task) = self.set_type_task.as_mut() {
if let Some(result) = task.poll() {
self.set_type_task = None;
self.message = Some(match result {
Ok(set) => format!("new members are created as {set}"),
Err(error) => error,
});
self.reload();
}
}
if self.generate_after_save && !self.save_request && !dirty && writes_pending == 0 {
self.generate_after_save = false;
self.request_generate(false, json());
}
let task = self.generate_task.as_mut()?;
let result = task.poll()?;
self.generate_task = None;
match result {
Ok(report) => {
self.message = None;
self.reload();
Some(report)
}
Err(error) => {
self.message = Some(format!("Generate was refused: {error}"));
None
}
}
}
pub fn show(&mut self, ui: &mut egui::Ui, table: &FamilyTable, hits: &mut std::collections::HashMap<String, egui::Rect>) -> Option<String> {
if !self.active() {
return None;
}
let mut picked = None;
ui.horizontal_wrapped(|ui| {
ui.label("Members are created as part type");
let field = ui.add(egui::TextEdit::singleline(&mut self.member_type_input).desired_width(110.0));
hits.insert("family:plm_member_type".into(), field.rect);
if let Some(info) = &self.info {
if !info.member_part_type_mode.is_empty() {
ui.weak(format!("({} numbering)", info.member_part_type_mode));
}
}
let changed = self.info.as_ref().is_some_and(|i| i.member_part_type != self.member_type_input.trim());
let set = ui.add_enabled(changed && self.set_type_task.is_none(), egui::Button::new("Set"));
hits.insert("family:plm_member_type_set".into(), set.rect);
if set.clicked() {
self.request_member_type();
}
});
if let Some(info) = &self.info {
let offered = family::keys_to_offer(&info.schema, table);
if !offered.is_empty() {
ui.horizontal_wrapped(|ui| {
ui.label("Category keys:");
for attribute in offered {
let button = ui.small_button(&attribute.key).on_hover_text(format!(
"Add {} ({}) as a column; it becomes each member's catalog value",
attribute.name, attribute.kind
));
hits.insert(format!("family:plm_key:{}", attribute.key), button.rect);
if button.clicked() {
picked = Some(attribute.key.clone());
}
}
});
}
let problems = family::attribute_column_problems(&info.schema, table);
for (i, problem) in problems.iter().enumerate() {
let label = ui.add(egui::Label::new(egui::RichText::new(problem).color(ui.visuals().warn_fg_color)).wrap());
hits.insert(format!("family:plm_attribute_problem:{i}"), label.rect);
}
}
if self.refused_unsaved {
let button = ui.button("Save and generate").on_hover_text("Save the family to the server, then Generate from it");
hits.insert("family:plm_save_and_generate".into(), button.rect);
if button.clicked() {
self.request_save_and_generate();
}
}
if let Some(message) = &self.message {
ui.add(egui::Label::new(egui::RichText::new(message).color(ui.visuals().weak_text_color())).wrap());
}
if self.busy() {
ui.ctx().request_repaint_after(std::time::Duration::from_millis(30));
}
picked
}
pub fn state_json(&self, table: Option<&FamilyTable>) -> serde_json::Value {
if !self.active() {
return serde_json::Value::Null;
}
let problems = match (&self.info, table) {
(Some(info), Some(table)) => family::attribute_column_problems(&info.schema, table),
_ => Vec::new(),
};
let keys: Vec<&str> = match (&self.info, table) {
(Some(info), Some(table)) => family::keys_to_offer(&info.schema, table).into_iter().map(|a| a.key.as_str()).collect(),
_ => Vec::new(),
};
serde_json::json!({
"key": self.key,
"number": self.info.as_ref().map(|i| i.number.clone()),
"memberPartType": self.info.as_ref().map(|i| i.member_part_type.clone()),
"memberPartTypeMode": self.info.as_ref().map(|i| i.member_part_type_mode.clone()),
"category": self.info.as_ref().map(|i| i.category.clone()),
"keysToOffer": keys,
"attributeProblems": problems,
"refusedUnsaved": self.refused_unsaved,
"busy": self.busy(),
"message": self.message,
})
}
}
async fn load_info(client: &PlmClient, key: &str) -> Result<Info, String> {
let (part, _) = family::key_ids(key).ok_or_else(|| format!("`{key}` is not a PLM revision"))?;
let head = family::part_head(client, &part).await.map_err(|e| e.to_string())?;
let view = family::family_view(client, &part).await.map_err(|e| e.to_string())?;
let schema = if head.category.is_empty() {
Vec::new()
} else {
let response = client
.call("GET", &format!("/api/categories/{}/schema", crate::http::query_component(&head.category)), None)
.await
.map_err(|e| e.to_string())?;
let schema: crate::panels::plm_parts::Schema =
serde_json::from_slice(&response.body).map_err(|e| format!("the category's schema: {e}"))?;
schema.attributes
};
Ok(Info {
number: head.number,
member_part_type: view.member_part_type,
member_part_type_mode: view.member_part_type_mode,
category: head.category,
schema,
})
}