1use crate::panels::plm_parts::Pending;
21use crate::plm::adoption::{
22 plan_adoption, run_import, walk_tree, AdoptionPlan, ImportLedgers, ImportNumbering, ImportReport, Ledger,
23 SourceDocument,
24};
25use crate::plm::client::PlmClient;
26use crate::plm::uses::{Disagreement, Repoint};
27use crate::store::ModelStore;
28use eframe::egui;
29use std::cell::{Cell, RefCell};
30use std::collections::HashMap;
31use std::rc::Rc;
32use std::task::Waker;
33
34pub struct Planned {
36 pub folder: String,
37 pub documents: Vec<SourceDocument>,
38 pub plan: AdoptionPlan,
39 canonical: HashMap<String, String>,
42}
43
44struct Running {
46 answer: Pending<ImportReport>,
47 ledger: Rc<RefCell<Ledger>>,
48 stop: Rc<Cell<bool>>,
49 saved: Ledger,
50}
51
52pub struct PlmImportPanel {
54 pub folder: String,
55 pub numbering: ImportNumbering,
56 pub planned: Option<Planned>,
57 pub report: Option<ImportReport>,
58 pub problem: Option<String>,
59 running: Option<Running>,
60 asked: Option<Ask>,
62 shown: Option<Ledger>,
65 local: bool,
67 pub hits: HashMap<String, egui::Rect>,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72enum Ask {
73 Plan,
74 Start,
75 Stop,
76}
77
78impl Default for PlmImportPanel {
79 fn default() -> Self {
80 Self {
81 folder: String::new(),
82 numbering: ImportNumbering { part_type: "component".into(), number_from_name: false },
83 planned: None,
84 report: None,
85 problem: None,
86 running: None,
87 asked: None,
88 shown: None,
89 local: true,
90 hits: HashMap::new(),
91 }
92 }
93}
94
95impl PlmImportPanel {
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn running(&self) -> bool {
101 self.running.is_some()
102 }
103
104 pub fn plan(&mut self, store: &dyn ModelStore) {
106 self.report = None;
107 self.problem = None;
108 match walk_tree(store, &self.folder) {
109 Ok(documents) => {
110 let mut canonical = HashMap::new();
111 for document in &documents {
112 let Ok(value) = serde_json::from_str::<serde_json::Value>(&document.contents) else {
113 continue;
114 };
115 collect_keys(&value, &mut |key| {
116 canonical.entry(key.to_string()).or_insert_with(|| store.canonical_identity(key));
117 });
118 }
119 let lookup = |key: &str| canonical.get(key).cloned().unwrap_or_else(|| key.to_string());
120 let plan = plan_adoption(&documents, &lookup);
121 self.planned = Some(Planned { folder: self.folder.clone(), documents, plan, canonical });
122 }
123 Err(problem) => {
124 self.planned = None;
125 self.problem = Some(problem);
126 }
127 }
128 }
129
130 pub fn start(&mut self, store: &dyn ModelStore, client: Rc<PlmClient>, server: &str) {
132 let Some(planned) = &self.planned else {
133 return;
134 };
135 let ledger = ImportLedgers::load(store).ledger(server, &planned.folder);
136 let shared = Rc::new(RefCell::new(ledger.clone()));
137 let stop = Rc::new(Cell::new(false));
138 let (plan, documents, canonical) = (planned.plan.clone(), planned.documents.clone(), planned.canonical.clone());
139 let numbering = self.numbering.clone();
140 let (progress, stopping) = (shared.clone(), stop.clone());
141 let future = async move {
142 let lookup = |key: &str| canonical.get(key).cloned().unwrap_or_else(|| key.to_string());
143 let mut ledger = ledger;
144 let report = run_import(&client, &plan, &documents, &mut ledger, &lookup, &numbering, &mut |moved| {
145 *progress.borrow_mut() = moved.clone();
146 !stopping.get()
147 })
148 .await;
149 Ok(report)
150 };
151 self.report = None;
152 self.running = Some(Running {
153 answer: Pending::new(Box::pin(future)),
154 ledger: shared,
155 stop,
156 saved: Ledger::default(),
157 });
158 }
159
160 pub fn stop(&mut self) {
162 if let Some(running) = &self.running {
163 running.stop.set(true);
164 }
165 }
166
167 pub fn poll(&mut self, store: &dyn ModelStore, server: &str, waker: &Waker) {
170 let Some(running) = &mut self.running else {
171 return;
172 };
173 let answer = running.answer.poll(waker);
174 let now = running.ledger.borrow().clone();
175 if now != running.saved {
176 if let Some(planned) = &self.planned {
177 let mut ledgers = ImportLedgers::load(store);
178 if let Err(problem) = ledgers.save(store, server, &planned.folder, &now) {
179 self.problem = Some(format!("the import's progress could not be saved: {problem}"));
180 }
181 }
182 running.saved = now;
183 }
184 if let Some(answer) = answer {
185 self.running = None;
186 match answer {
187 Ok(report) => self.report = Some(report),
188 Err(problem) => self.problem = Some(problem),
189 }
190 }
191 }
192
193 pub fn sync(&mut self, store: &dyn ModelStore, files: Option<&dyn ModelStore>, client: Option<Rc<PlmClient>>, server: &str, waker: &Waker) {
203 self.local = files.is_some();
204 if self.folder.is_empty() {
205 if let Some(files) = files {
206 self.folder = files.browser_location();
207 }
208 }
209 match self.asked.take() {
210 Some(Ask::Plan) => match files {
211 Some(files) => self.plan(files),
212 None => self.problem = Some("importing a folder needs the desktop app: this session has no local folders".into()),
213 },
214 Some(Ask::Start) => {
215 if let Some(client) = client {
216 self.start(store, client, server);
217 }
218 }
219 Some(Ask::Stop) => self.stop(),
220 None => {}
221 }
222 self.poll(store, server, waker);
223 self.shown = match (&self.running, &self.planned) {
224 (Some(running), _) => Some(running.ledger.borrow().clone()),
225 (None, Some(planned)) => Some(ImportLedgers::load(store).ledger(server, &planned.folder)),
226 (None, None) => None,
227 };
228 }
229
230 pub fn state_json(&self) -> serde_json::Value {
233 let plan = self.planned.as_ref().map(|p| &p.plan);
234 serde_json::json!({
235 "folder": self.folder,
236 "local": self.local,
237 "documents": self.planned.as_ref().map_or(0, |p| p.documents.len()),
238 "parts": plan.map_or(0, |p| p.parts.len()),
239 "assemblies": plan.map_or(0, |p| p.parts.iter().filter(|x| x.assembly).count()),
240 "refused": plan.map_or(0, |p| p.refused.len()),
241 "external": plan.map_or(0, |p| p.external.len()),
242 "running": self.running(),
243 "report": self.report.as_ref().map(|r| serde_json::json!({
244 "minted": r.minted, "uploaded": r.uploaded, "published": r.published,
245 "refused": r.refused.iter().map(|(d, why)| format!("{d}: {why}")).collect::<Vec<_>>(),
246 })),
247 "problem": self.problem,
248 })
249 }
250
251 pub fn draw(&mut self, ui: &mut egui::Ui, can_import: bool) {
255 self.hits.clear();
256 if !self.local {
257 ui.weak("Importing a folder of documents runs in the desktop app: this session has no local folders.");
258 return;
259 }
260 ui.horizontal(|ui| {
261 ui.label("Folder");
262 let field = ui.add_enabled(!self.running(), egui::TextEdit::singleline(&mut self.folder));
263 self.hits.insert("plm_import:folder".into(), field.rect);
264 let plan = ui.add_enabled(!self.running(), egui::Button::new("Read folder"));
265 self.hits.insert("plm_import:plan".into(), plan.rect);
266 if plan.clicked() {
267 self.asked = Some(Ask::Plan);
268 }
269 });
270 ui.horizontal(|ui| {
271 ui.label("Part type");
272 ui.add_enabled(!self.running(), egui::TextEdit::singleline(&mut self.numbering.part_type).desired_width(120.0));
273 ui.add_enabled(
274 !self.running(),
275 egui::Checkbox::new(&mut self.numbering.number_from_name, "number each part by its file name"),
276 );
277 });
278 if let Some(planned) = &self.planned {
279 let plan = &planned.plan;
280 let assemblies = plan.parts.iter().filter(|p| p.assembly).count();
281 ui.label(format!(
282 "{} document{} → {} part{}, {} with a uses list",
283 planned.documents.len(),
284 plural(planned.documents.len()),
285 plan.parts.len(),
286 plural(plan.parts.len()),
287 assemblies
288 ));
289 for (document, reason) in &plan.refused {
290 ui.colored_label(ui.visuals().warn_fg_color, format!("not imported: {document} — {reason}"));
291 }
292 for (document, key) in &plan.external {
293 ui.colored_label(
294 ui.visuals().warn_fg_color,
295 format!("{document} uses '{key}', which is not in this folder: its uses list will be refused"),
296 );
297 }
298 if let Some(ledger) = &self.shown {
299 let minted = ledger.entries.len();
300 let uploaded = ledger.entries.values().filter(|e| e.uploaded).count();
301 let published = ledger.entries.values().filter(|e| e.published).count();
302 ui.weak(format!(
303 "on this server: {minted}/{} minted, {uploaded}/{} uploaded, {published}/{assemblies} uses lists",
304 plan.parts.len(),
305 plan.parts.len()
306 ));
307 }
308 }
309 ui.horizontal(|ui| match (can_import, self.running()) {
310 (_, true) => {
311 ui.spinner();
312 let stop = ui.button("Stop");
313 self.hits.insert("plm_import:stop".into(), stop.rect);
314 if stop.clicked() {
315 self.asked = Some(Ask::Stop);
316 }
317 }
318 (true, false) => {
319 let run = ui.add_enabled(self.planned.is_some(), egui::Button::new("Import"));
320 self.hits.insert("plm_import:run".into(), run.rect);
321 if run.clicked() {
322 self.asked = Some(Ask::Start);
323 }
324 }
325 (false, false) => {
326 ui.weak("Sign in to a PLM to import.");
327 }
328 });
329 if let Some(report) = &self.report {
330 ui.label(format!(
331 "minted {}, uploaded {}, published {}",
332 report.minted, report.uploaded, report.published
333 ));
334 for (document, sentence) in &report.refused {
335 ui.colored_label(ui.visuals().error_fg_color, format!("{document}: {sentence}"));
336 }
337 }
338 if let Some(problem) = &self.problem {
339 ui.colored_label(ui.visuals().error_fg_color, problem);
340 }
341 }
342
343 pub fn show(&mut self, ui: &mut egui::Ui, store: &dyn ModelStore, client: Option<Rc<PlmClient>>, server: &str) {
347 let waker = repaint_waker(ui.ctx());
348 self.sync(store, Some(store), client.clone(), server, &waker);
349 self.draw(ui, client.is_some());
350 self.sync(store, Some(store), client, server, &waker);
351 }
352}
353
354fn collect_keys(document: &serde_json::Value, found: &mut dyn FnMut(&str)) {
355 let Some(library) = document.get("partsLibrary").and_then(serde_json::Value::as_object) else {
356 return;
357 };
358 for entry in library.values() {
359 if let Some(key) = entry.get("sourceKey").and_then(serde_json::Value::as_str) {
360 if !key.is_empty() {
361 found(key);
362 }
363 }
364 if let Some(nested) = entry.get("document") {
365 collect_keys(nested, found);
366 }
367 }
368}
369
370fn plural(count: usize) -> &'static str {
371 if count == 1 {
372 ""
373 } else {
374 "s"
375 }
376}
377
378fn repaint_waker(ctx: &egui::Context) -> Waker {
379 struct Repaint(egui::Context);
380 impl std::task::Wake for Repaint {
381 fn wake(self: std::sync::Arc<Self>) {
382 self.0.request_repaint();
383 }
384 }
385 Waker::from(std::sync::Arc::new(Repaint(ctx.clone())))
386}
387
388#[derive(Clone, Debug, PartialEq)]
392pub enum RepointChoice {
393 Repoint(Vec<Repoint>),
396 KeepDocument,
399}
400
401pub struct RepointPrompt;
404
405impl RepointPrompt {
406 pub fn show(
407 ui: &mut egui::Ui,
408 found: &Disagreement,
409 names: &dyn Fn(&str) -> String,
410 hits: &mut HashMap<String, egui::Rect>,
411 ) -> Option<RepointChoice> {
412 ui.label("This assembly's parts list on the PLM no longer matches the document:");
413 for repoint in &found.repoints {
414 ui.label(format!(
415 "• {} ({}) → {}",
416 names(&repoint.from.0),
417 repoint.occurrences.join(", "),
418 names(&repoint.to.0)
419 ));
420 }
421 for placed in &found.unlisted {
422 ui.weak(format!("• placed here but not on the list: {} ×{}", names(&placed.line.part), placed.line.quantity));
423 }
424 for listed in &found.unplaced {
425 ui.weak(format!("• on the list but not placed: {} ×{}", names(&listed.part), listed.quantity));
426 }
427 let mut choice = None;
428 ui.horizontal(|ui| {
429 if !found.repoints.is_empty() {
430 let button = ui.button("Re-point to match the PLM");
431 hits.insert("plm_import:repoint:apply".into(), button.rect);
432 if button.clicked() {
433 choice = Some(RepointChoice::Repoint(found.repoints.clone()));
434 }
435 }
436 let keep = ui.button("Keep the document");
437 hits.insert("plm_import:repoint:keep".into(), keep.rect);
438 if keep.clicked() {
439 choice = Some(RepointChoice::KeepDocument);
440 }
441 });
442 ui.weak("Keeping it means the next save publishes the parts list from the document again.");
443 choice
444 }
445}
446
447pub fn revision_key_of(name: &str) -> Option<String> {
451 crate::plm::uses::revision_key_in(name)
452}
453
454#[derive(Default)]
459pub struct RepointCheck {
460 checked: std::collections::HashSet<u64>,
462 pending: HashMap<u64, Pending<Disagreement>>,
463 found: HashMap<u64, Disagreement>,
465 pub hits: HashMap<String, egui::Rect>,
466}
467
468impl RepointCheck {
469 pub fn new() -> Self {
470 Self::default()
471 }
472
473 pub fn sync(&mut self, client: &Rc<PlmClient>, docs: &crate::document::Documents, waker: &Waker) {
476 let open: Vec<u64> = docs.iter().map(|d| d.id()).collect();
477 self.checked.retain(|id| open.contains(id));
478 self.pending.retain(|id, _| open.contains(id));
479 self.found.retain(|id, _| open.contains(id));
480 for doc in docs.iter() {
481 if self.checked.contains(&doc.id()) {
482 continue;
483 }
484 let Some(key) = doc.name().and_then(revision_key_of) else {
485 continue;
486 };
487 self.checked.insert(doc.id());
488 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history_request_json()) else {
489 continue;
490 };
491 if !crate::plm::uses::publishes_on_save(&document) {
492 continue;
493 }
494 let client = client.clone();
495 self.pending.insert(
496 doc.id(),
497 Pending::new(Box::pin(async move { crate::plm::uses::check_on_open(&client, &key, &document).await })),
498 );
499 }
500 let answered: Vec<(u64, Result<Disagreement, String>)> = self
501 .pending
502 .iter_mut()
503 .filter_map(|(id, pending)| pending.poll(waker).map(|answer| (*id, answer)))
504 .collect();
505 for (id, answer) in answered {
506 self.pending.remove(&id);
507 if let Ok(found) = answer {
510 if !found.is_empty() {
511 self.found.insert(id, found);
512 }
513 }
514 }
515 }
516
517 pub fn busy(&self) -> bool {
518 !self.pending.is_empty()
519 }
520
521 pub fn disagreement(&self, id: u64) -> Option<&Disagreement> {
523 self.found.get(&id)
524 }
525
526 pub fn answer(&mut self, docs: &mut crate::document::Documents, id: u64, choice: RepointChoice) -> Result<(), String> {
531 self.found.remove(&id);
532 let RepointChoice::Repoint(repoints) = choice else {
533 return Ok(());
534 };
535 let Some(doc) = docs.iter_mut().find(|d| d.id() == id) else {
536 return Ok(());
537 };
538 let mut document: serde_json::Value =
539 serde_json::from_str(&doc.engine.history_request_json()).map_err(|e| e.to_string())?;
540 for repoint in &repoints {
541 crate::plm::uses::repoint(&mut document, repoint, &crate::plm::uses::plm_identity);
542 }
543 doc.engine.edit_document_json(&document.to_string()).map(|_| ())
544 }
545
546 pub fn ui(&mut self, ui: &mut egui::Ui, docs: &mut crate::document::Documents, names: &dyn Fn(&str) -> String) -> Result<(), String> {
550 self.hits.clear();
551 let id = docs.active().id();
552 let Some(found) = self.found.get(&id).cloned() else {
553 return Ok(());
554 };
555 match RepointPrompt::show(ui, &found, names, &mut self.hits) {
556 Some(choice) => self.answer(docs, id, choice),
557 None => Ok(()),
558 }
559 }
560}
561
562#[derive(Default)]
579pub struct CachedCopyCheck {
580 checked: std::collections::HashSet<u64>,
581 pending: HashMap<u64, Pending<HashMap<String, serde_json::Value>>>,
583 unread: HashMap<u64, usize>,
586 refreshed: HashMap<u64, Vec<String>>,
588 due: HashMap<u64, Vec<String>>,
591 live: HashMap<u64, Vec<String>>,
593 asking: HashMap<u64, (Vec<String>, HashMap<String, serde_json::Value>)>,
596 kept: HashMap<u64, HashMap<String, String>>,
599 updated: u64,
601 quiet: std::collections::HashSet<u64>,
604}
605
606impl CachedCopyCheck {
607 pub fn new() -> Self {
608 Self::default()
609 }
610
611 pub fn sync(&mut self, client: &Rc<PlmClient>, store: &dyn ModelStore, docs: &mut crate::document::Documents, waker: &Waker, writing: bool) {
614 let open: Vec<u64> = docs.iter().map(|d| d.id()).collect();
615 self.checked.retain(|id| open.contains(id));
616 self.pending.retain(|id, _| open.contains(id));
617 self.unread.retain(|id, _| open.contains(id));
618 self.refreshed.retain(|id, _| open.contains(id));
619 self.due.retain(|id, _| open.contains(id));
620 self.live.retain(|id, _| open.contains(id));
621 self.asking.retain(|id, _| open.contains(id));
622 self.kept.retain(|id, _| open.contains(id));
623 self.quiet.retain(|id| open.contains(id));
624 if !writing {
625 for (id, keys) in std::mem::take(&mut self.due) {
626 if self.pending.contains_key(&id) {
627 self.due.insert(id, keys);
628 } else {
629 self.checked.remove(&id);
630 self.live.insert(id, keys);
631 }
632 }
633 }
634 for doc in docs.iter() {
635 if self.checked.contains(&doc.id()) || doc.name().and_then(revision_key_of).is_none() {
636 continue;
637 }
638 self.checked.insert(doc.id());
639 if doc.is_recovered() {
644 continue;
645 }
646 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history_request_json()) else {
647 continue;
648 };
649 if !document["partsLibrary"].as_object().is_some_and(|l| !l.is_empty()) {
650 continue;
651 }
652 let mut unchanged = crate::plm::cached_copies::unchanged(&document, &|key| {
656 store.plm_revision(key).map(|entry| entry.content_hash)
657 });
658 match self.live.get(&doc.id()) {
663 Some(named) if named.is_empty() => unchanged.clear(),
664 Some(named) => unchanged.retain(|key, _| !named.contains(key) || store.plm_written_by_us(key)),
665 None => {}
666 }
667 self.unread.insert(doc.id(), unchanged.len());
668 let client = client.clone();
669 self.pending.insert(
670 doc.id(),
671 Pending::new(Box::pin(async move { Ok(crate::plm::cached_copies::fetch_changed(&client, &document, unchanged).await) })),
672 );
673 }
674 let answered: Vec<(u64, Result<HashMap<String, serde_json::Value>, String>)> = self
675 .pending
676 .iter_mut()
677 .filter_map(|(id, pending)| pending.poll(waker).map(|answer| (*id, answer)))
678 .collect();
679 for (id, answer) in answered {
680 self.pending.remove(&id);
681 if let Some(moved) = self.live.remove(&id) {
682 if writing {
683 self.due.insert(id, moved);
685 continue;
686 }
687 if let (Ok(sources), Some(doc)) = (answer, docs.iter_mut().find(|d| d.id() == id)) {
688 if let Some(paths) = self.follow(doc, &sources) {
689 self.asking.insert(id, (paths, sources));
690 }
691 }
692 continue;
693 }
694 let (Ok(sources), Some(doc)) = (answer, docs.iter_mut().find(|d| d.id() == id)) else {
695 continue;
696 };
697 let Ok(mut document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history_request_json()) else {
700 continue;
701 };
702 let before = document.clone();
703 let quiet = self.quiet.remove(&id);
704 for (key, why) in crate::document::owned_save::unsaved_edits(&document).into_iter().filter(|_| !quiet) {
707 let name = crate::document::owned_save::first_copy(&document, &key, &mut Vec::new())
708 .map_or_else(|| key.clone(), |(names, _)| names.join("/"));
709 doc.engine.push_notice(format!(
710 "{name} has changes that are not saved to its PLM item yet ({why}): {}. They are kept in this assembly until then",
711 crate::plm::cached_copies::way_forward(&why)
712 ));
713 }
714 let replaced = crate::plm::cached_copies::refresh(&mut document, &sources);
715 if replaced.is_empty() {
716 continue;
717 }
718 let key = doc.edit_key();
719 match doc.engine.edit_document_json(&document.to_string()) {
720 Ok(_) => {
724 doc.absorb_open_write(&before, &document, key);
725 doc.arm_open_settle();
727 if !quiet { doc.engine.push_notice(format!(
728 "Updated {} from {} PLM revision{}: the copies saved in this assembly were older",
729 replaced.join(", "),
730 if replaced.len() == 1 { "its" } else { "their" },
731 if replaced.len() == 1 { "" } else { "s" }
732 )); }
733 }
734 Err(problem) => doc.engine.push_notice(format!("Could not update {} from the PLM: {problem}", replaced.join(", "))),
735 }
736 self.refreshed.insert(id, replaced);
737 }
738 }
739
740 pub fn recheck(&mut self, id: u64) {
743 self.quiet.insert(id);
744 self.checked.remove(&id);
745 self.pending.remove(&id);
746 self.refreshed.remove(&id);
747 self.live.remove(&id);
748 }
749
750 pub fn moved(&mut self, docs: &crate::document::Documents, keys: &[String], stale: bool) {
753 let named: std::collections::BTreeSet<String> = keys.iter().filter_map(|k| revision_key_of(k)).collect();
754 for doc in docs.iter() {
755 if !self.checked.contains(&doc.id()) {
756 continue;
757 }
758 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history_request_json()) else {
759 continue;
760 };
761 let embedded = crate::plm::cached_copies::revision_keys(&document);
762 let hit: Vec<String> = embedded.into_iter().filter(|k| named.contains(k)).collect();
763 if stale || !hit.is_empty() {
764 self.due.entry(doc.id()).or_default().extend(hit);
765 }
766 }
767 }
768
769 fn follow(&mut self, doc: &mut crate::document::Document, sources: &HashMap<String, serde_json::Value>) -> Option<Vec<String>> {
773 use crate::plm::cached_copies::{moved_revisions, paths_of, refresh_except};
774 let mut baseline = doc.saved_document()?;
777 let mut current: serde_json::Value = serde_json::from_str(&doc.engine.history.request_json()).ok()?;
781 let edited: std::collections::BTreeSet<String> = crate::document::owned_save::owned_changes(&baseline, ¤t)
782 .writes
783 .into_iter()
784 .filter_map(|w| revision_key_of(&w.key))
785 .collect();
786 let moved = moved_revisions(&baseline, sources);
789 let changed = paths_of(¤t, &moved);
790 let kept = self.kept.get(&doc.id());
791 let ask: std::collections::BTreeSet<String> = moved
792 .intersection(&edited)
793 .filter(|key| kept.and_then(|k| k.get(*key)) != sources.get(*key).map(|s| s.to_string()).as_ref())
794 .cloned()
795 .collect();
796 let asked = paths_of(¤t, &ask);
797 let replaced = refresh_except(&mut current, sources, &edited);
798 if !replaced.is_empty() {
799 refresh_except(&mut baseline, sources, &edited);
800 let clean = !doc.is_dirty();
801 let key = doc.edit_key();
802 match doc.engine.edit_document_json(¤t.to_string()) {
803 Ok(_) => {
804 doc.rebase_saved_document(&baseline, key);
805 if clean {
806 doc.arm_open_settle();
809 }
810 self.updated += 1;
811 let what = if changed.is_empty() { replaced.join(", ") } else { changed.join(", ") };
812 doc.engine.push_notice(format!(
813 "{what} changed on the PLM (saved by another client): updated {} in this assembly",
814 replaced.join(", ")
815 ));
816 }
817 Err(problem) => doc.engine.push_notice(format!("Could not update {} from the PLM: {problem}", replaced.join(", "))),
818 }
819 }
820 (!asked.is_empty()).then_some(asked)
821 }
822
823 pub fn asking(&self, id: u64) -> Option<&[String]> {
825 self.asking.get(&id).map(|(paths, _)| paths.as_slice())
826 }
827
828 pub fn answer(&mut self, docs: &mut crate::document::Documents, id: u64, take_theirs: bool) {
832 let Some((paths, sources)) = self.asking.remove(&id) else { return };
833 if !take_theirs {
834 let kept = self.kept.entry(id).or_default();
835 for (key, source) in &sources {
836 kept.insert(key.clone(), source.to_string());
837 }
838 return;
839 }
840 let Some(doc) = docs.iter_mut().find(|d| d.id() == id) else { return };
841 let (Some(mut baseline), Ok(mut current)) =
842 (doc.saved_document(), serde_json::from_str::<serde_json::Value>(&doc.engine.history.request_json()))
843 else {
844 return;
845 };
846 let replaced = crate::plm::cached_copies::refresh(&mut current, &sources);
847 if replaced.is_empty() {
848 return;
849 }
850 crate::plm::cached_copies::refresh(&mut baseline, &sources);
851 let key = doc.edit_key();
852 match doc.engine.edit_document_json(¤t.to_string()) {
853 Ok(_) => {
854 doc.rebase_saved_document(&baseline, key);
855 if !doc.is_dirty() {
856 doc.arm_open_settle();
859 }
860 doc.arm_converge();
864 self.updated += 1;
865 doc.engine.push_notice(format!(
866 "{} now {} the other client's version; your edits there were discarded",
867 paths.join(", "),
868 if paths.len() == 1 { "shows" } else { "show" }
869 ));
870 }
871 Err(problem) => doc.engine.push_notice(format!("Could not update {} from the PLM: {problem}", replaced.join(", "))),
872 }
873 }
874
875 pub fn busy(&self) -> bool {
876 !self.pending.is_empty()
877 }
878
879 pub fn live_json(&self, id: u64) -> serde_json::Value {
881 serde_json::json!({
882 "updated": self.updated,
883 "due": self.due.contains_key(&id) || self.live.contains_key(&id),
884 "asking": self.asking(id).unwrap_or(&[]),
885 })
886 }
887
888 pub fn refreshed(&self, id: u64) -> &[String] {
890 self.refreshed.get(&id).map_or(&[], Vec::as_slice)
891 }
892
893 pub fn unread(&self, id: u64) -> usize {
896 self.unread.get(&id).copied().unwrap_or(0)
897 }
898}
899
900#[derive(Debug, Clone)]
904pub struct Superseded {
905 pub key: String,
907 pub current_key: String,
908 pub number: String,
909 pub from: String,
910 pub lifecycle: String,
911 pub to: String,
912 text: String,
914 pub paths: Vec<String>,
916}
917
918#[derive(Default)]
927pub struct SupersededCheck {
928 checked: std::collections::HashSet<u64>,
929 waits: HashMap<u64, u32>,
931 pending: HashMap<u64, Pending<Vec<Superseded>>>,
932 found: HashMap<u64, Vec<Superseded>>,
933 asking: std::collections::HashSet<u64>,
935 updated: u64,
936 reads: u64,
938 keeps: HashMap<u64, String>,
940 creating: HashMap<u64, Pending<(String, String)>>,
942 auto: std::collections::HashSet<String>,
945 opened: Vec<(u64, String)>,
947 pinned: HashMap<u64, Vec<(String, bool)>>,
952}
953
954const INDEX_WAIT_FRAMES: u32 = 200;
957
958impl SupersededCheck {
959 pub fn sync(&mut self, client: &Rc<PlmClient>, store: &dyn ModelStore, docs: &mut crate::document::Documents, waker: &Waker) {
960 use crate::plm::superseded::{current_release, is_superseded, pinned_keys};
961 let open: Vec<u64> = docs.iter().map(|d| d.id()).collect();
962 self.checked.retain(|id| open.contains(id));
963 self.waits.retain(|id, _| open.contains(id));
964 self.pending.retain(|id, _| open.contains(id));
965 self.found.retain(|id, _| open.contains(id));
966 self.asking.retain(|id| open.contains(id));
967 self.keeps.retain(|id, _| open.contains(id));
968 self.creating.retain(|id, _| open.contains(id));
969 self.pinned.retain(|id, _| open.contains(id));
970 let moved: Vec<u64> = self
973 .pinned
974 .iter()
975 .filter(|(id, keys)| {
976 !self.pending.contains_key(id)
977 && keys.iter().any(|(key, was)| !was && store.plm_revision(key).is_some_and(|e| is_superseded(&e.lifecycle)))
978 })
979 .map(|(id, _)| *id)
980 .collect();
981 for id in moved {
982 self.pinned.remove(&id);
983 self.checked.remove(&id);
984 self.waits.remove(&id);
985 }
986 let made: Vec<(u64, Result<(String, String), String>)> =
987 self.creating.iter_mut().filter_map(|(id, pending)| pending.poll(waker).map(|answer| (*id, answer))).collect();
988 for (id, answer) in made {
989 self.creating.remove(&id);
990 let Some(doc) = docs.iter_mut().find(|d| d.id() == id) else { continue };
991 match answer {
992 Ok((key, text)) => {
993 store.remember_saved_document(&store.canonical_identity(&key), &text);
996 store.refresh_plm_index(std::slice::from_ref(&key), false);
997 if let Some(key) = revision_key_of(&key) {
998 self.auto.insert(key.clone());
999 self.opened.push((id, key));
1000 }
1001 doc.engine.push_notice_as(
1002 brep_render::engine_state::NoticeSeverity::Info,
1003 "Made a new revision of this assembly, checked out to you: opening it with the current revisions",
1004 );
1005 }
1006 Err(problem) => doc.engine.push_notice(format!("Could not make a new revision of this assembly: {problem}")),
1007 }
1008 }
1009 let mut apply_now = Vec::new();
1010 for doc in docs.iter() {
1011 let id = doc.id();
1012 if self.checked.contains(&id) || self.pending.contains_key(&id) || doc.name().and_then(revision_key_of).is_none() {
1013 continue;
1014 }
1015 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history.request_json()) else { continue };
1016 let keys = pinned_keys(&document);
1017 let entries: Vec<(String, Option<crate::plm::client::IndexEntry>)> =
1018 keys.into_iter().map(|k| { let e = store.plm_revision(&k); (k, e) }).collect();
1019 if entries.iter().any(|(_, e)| e.is_none()) {
1020 let waited = self.waits.entry(id).or_default();
1021 *waited += 1;
1022 if *waited < INDEX_WAIT_FRAMES {
1023 continue;
1024 }
1025 }
1026 self.checked.insert(id);
1027 self.pinned.insert(
1028 id,
1029 entries.iter().map(|(key, entry)| (key.clone(), entry.as_ref().is_some_and(|e| is_superseded(&e.lifecycle)))).collect(),
1030 );
1031 let mut by_part: std::collections::BTreeMap<String, Vec<(String, crate::plm::client::IndexEntry)>> = Default::default();
1033 for (key, entry) in entries {
1034 if let Some(entry) = entry.filter(|e| is_superseded(&e.lifecycle)) {
1035 by_part.entry(entry.part_id.clone()).or_default().push((key, entry));
1036 }
1037 }
1038 if by_part.is_empty() {
1039 continue;
1040 }
1041 self.reads += 2 * by_part.len() as u64;
1042 let client = client.clone();
1043 self.pending.insert(
1044 id,
1045 Pending::new(Box::pin(async move {
1046 let mut out = Vec::new();
1047 for (part, stale) in by_part {
1048 let path = format!("/api/parts/{}", crate::plm::identity::segment(&part));
1049 let answer = client.call("GET", &path, None).await.map_err(|e| e.to_string())?;
1050 let view: serde_json::Value = serde_json::from_slice(&answer.body).map_err(|e| e.to_string())?;
1051 let Some((revision, label)) = current_release(&view) else {
1052 for (key, entry) in stale {
1055 out.push(Superseded {
1056 key,
1057 current_key: String::new(),
1058 number: entry.part_number.clone(),
1059 from: entry.revision_label.clone(),
1060 lifecycle: entry.lifecycle.clone(),
1061 to: String::new(),
1062 text: String::new(),
1063 paths: Vec::new(),
1064 });
1065 }
1066 continue;
1067 };
1068 let current_key = crate::plm::identity::document_key(&part, &revision);
1069 let Some(bytes) = client.get_document(¤t_key).await.map_err(|e| e.to_string())? else { continue };
1070 let text = String::from_utf8(bytes).map_err(|e| e.to_string())?;
1071 for (key, entry) in stale {
1072 if key == current_key {
1073 continue;
1074 }
1075 out.push(Superseded {
1076 key,
1077 current_key: current_key.clone(),
1078 number: entry.part_number.clone(),
1079 from: entry.revision_label.clone(),
1080 lifecycle: entry.lifecycle.clone(),
1081 to: label.clone(),
1082 text: text.clone(),
1083 paths: Vec::new(),
1084 });
1085 }
1086 }
1087 Ok(out)
1088 })),
1089 );
1090 }
1091 let answered: Vec<(u64, Result<Vec<Superseded>, String>)> =
1092 self.pending.iter_mut().filter_map(|(id, pending)| pending.poll(waker).map(|answer| (*id, answer))).collect();
1093 for (id, answer) in answered {
1094 self.pending.remove(&id);
1095 let Some(doc) = docs.iter_mut().find(|d| d.id() == id) else { continue };
1096 let mut found = match answer {
1097 Ok(found) => found,
1098 Err(problem) => {
1099 doc.engine.push_notice(format!("Could not check this assembly's PLM revisions: {problem}"));
1100 continue;
1101 }
1102 };
1103 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history.request_json()) else { continue };
1104 for item in &mut found {
1107 let mut names: Vec<String> = Vec::new();
1108 for placement in crate::plm::superseded::occurrences_of(&document, &item.key) {
1109 let name = placement.name();
1110 if !names.contains(&name) {
1111 names.push(name);
1112 }
1113 }
1114 item.paths = names;
1115 }
1116 found.retain(|item| !item.paths.is_empty());
1117 let (retired, rest): (Vec<Superseded>, Vec<Superseded>) = found.into_iter().partition(|item| item.current_key.is_empty());
1119 found = rest;
1120 for item in &retired {
1121 doc.engine.push_notice_as(
1122 brep_render::engine_state::NoticeSeverity::Warning,
1123 format!(
1124 "{}: {} uses {} rev {}, and no revision of it is released now: replace it with another part, or have a revision of it released",
1125 item.paths.join(", "),
1126 item.number,
1127 item.lifecycle,
1128 item.from
1129 ),
1130 );
1131 }
1132 if found.is_empty() {
1133 continue;
1134 }
1135 let own_key = doc.name().and_then(revision_key_of);
1139 let made_here = own_key.as_ref().is_some_and(|k| self.auto.contains(k));
1142 let own = own_key.as_ref().and_then(|key| store.plm_revision(key));
1143 let keeps = match &own {
1144 _ if made_here => None,
1145 Some(e) if e.lifecycle == "draft" && (e.locked_by.is_none() || e.locked_by_me) => None,
1146 Some(e) if e.lifecycle == "draft" => Some(format!("it is checked out by {}", e.locked_by.clone().unwrap_or_default())),
1147 Some(e) => Some(format!("it is {}", e.lifecycle)),
1148 None => Some("its PLM revision could not be read".to_string()),
1149 };
1150 for item in &found {
1151 let tail = keeps.as_ref().map_or(String::new(), |why| {
1152 format!(". This assembly keeps rev {} ({why}); make a new revision of it to use rev {}", item.from, item.to)
1153 });
1154 doc.engine.push_notice_as(
1155 brep_render::engine_state::NoticeSeverity::Warning,
1156 format!("{}: {} uses {} rev {}; {} is current{tail}", item.paths.join(", "), item.number, item.lifecycle, item.from, item.to),
1157 );
1158 }
1159 match &keeps {
1160 None if own_key.as_ref().is_some_and(|k| self.auto.remove(k)) => apply_now.push(id),
1162 None => {
1163 self.asking.insert(id);
1164 }
1165 Some(why) => {
1166 self.keeps.insert(id, why.clone());
1167 }
1168 }
1169 self.found.insert(id, found);
1170 }
1171 for id in apply_now {
1172 self.answer(docs, store, id, true);
1173 }
1174 }
1175
1176 pub fn keeps(&self, id: u64) -> Option<&str> {
1179 self.keeps.get(&id).map(String::as_str).filter(|_| !self.found(id).is_empty())
1180 }
1181
1182 pub fn creating(&self, id: u64) -> bool {
1183 self.creating.contains_key(&id)
1184 }
1185
1186 pub fn make_new_revision(&mut self, client: &Rc<PlmClient>, docs: &crate::document::Documents, id: u64) {
1190 if self.creating.contains_key(&id) {
1191 return;
1192 }
1193 let Some(doc) = docs.iter().find(|d| d.id() == id) else { return };
1194 let Some((part, _)) = doc.name().and_then(crate::panels::plm_host::revision_of) else { return };
1195 let text = doc.engine.history_request_json();
1196 let client = client.clone();
1197 self.creating.insert(
1198 id,
1199 Pending::new(Box::pin(async move {
1200 let key = crate::panels::plm_parts::save_as_new_revision(&client, &part, "", &text).await?;
1201 Ok((key, text))
1202 })),
1203 );
1204 }
1205
1206 pub fn take_opened(&mut self) -> Vec<(u64, String)> {
1208 std::mem::take(&mut self.opened)
1209 }
1210
1211 pub fn found(&self, id: u64) -> &[Superseded] {
1213 self.found.get(&id).map_or(&[], Vec::as_slice)
1214 }
1215
1216 pub fn asking(&self, id: u64) -> bool {
1217 self.asking.contains(&id) && !self.found(id).is_empty()
1218 }
1219
1220 pub fn answer(&mut self, docs: &mut crate::document::Documents, store: &dyn ModelStore, id: u64, update: bool) {
1225 use crate::panels::replace_component::{replace, replace_nested, ReplaceSource};
1226 self.asking.remove(&id);
1227 if !update {
1228 return;
1229 }
1230 let Some(found) = self.found.remove(&id) else { return };
1231 let Some(doc) = docs.iter_mut().find(|d| d.id() == id) else { return };
1232 for item in &found {
1233 store.remember_saved_document(&store.canonical_identity(&item.current_key), &item.text);
1234 let Ok(document) = serde_json::from_str::<serde_json::Value>(&doc.engine.history.request_json()) else { continue };
1235 let placements = crate::plm::superseded::occurrences_of(&document, &item.key);
1236 let source = ReplaceSource::Stored(item.current_key.clone());
1237 let top: Vec<String> = placements.iter().filter(|p| p.owners.is_empty()).map(|p| p.path.clone()).collect();
1238 let mut result = Ok(());
1239 if !top.is_empty() {
1240 result = replace(&mut doc.engine, store, &top, &source).map(|_| ());
1241 }
1242 let mut chains: Vec<&Vec<String>> = Vec::new();
1245 for placement in placements.iter().filter(|p| !p.owners.is_empty()) {
1246 if result.is_err() || chains.contains(&&placement.owners) {
1247 continue;
1248 }
1249 chains.push(&placement.owners);
1250 result = replace_nested(&mut doc.engine, store, &placement.path, &source, true).map(|_| ());
1251 }
1252 match result {
1253 Ok(()) => {
1254 self.updated += 1;
1255 doc.engine.push_notice_as(
1256 brep_render::engine_state::NoticeSeverity::Info,
1257 format!("{}: {} updated from rev {} to rev {}", item.paths.join(", "), item.number, item.from, item.to),
1258 );
1259 }
1260 Err(problem) => doc.engine.push_notice(format!("{}: could not update {} to rev {}: {problem}", item.paths.join(", "), item.number, item.to)),
1261 }
1262 }
1263 }
1264
1265 pub fn busy(&self) -> bool {
1266 !self.pending.is_empty() || !self.creating.is_empty()
1267 }
1268
1269 pub fn state_json(&self, id: u64) -> serde_json::Value {
1270 serde_json::json!({
1271 "checked": self.checked.contains(&id) && !self.pending.contains_key(&id),
1272 "superseded": self.found(id).iter().map(|s| serde_json::json!({
1273 "paths": s.paths, "number": s.number, "from": s.from, "lifecycle": s.lifecycle, "to": s.to,
1274 })).collect::<Vec<_>>(),
1275 "asking": self.asking(id),
1276 "keeps": self.keeps(id),
1277 "creating": self.creating(id),
1278 "updated": self.updated,
1279 "reads": self.reads,
1280 })
1281 }
1282}
1283