//! Transient browsing over the complete trace/artifact inventory. use oa_core::{Id, Project}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub(crate) enum Filter { #[default] All, Traces, Artifacts, } #[derive(Debug, Clone)] pub(crate) enum Event { Filter(Filter), Search(String), } #[derive(Default)] pub(crate) struct State { pub filter: Filter, pub query: String, } impl State { pub fn update(&mut self, event: Event) { match event { Event::Filter(filter) => self.filter = filter, Event::Search(query) => self.query = query, } } pub fn matching(&self, p: &Project) -> (Vec, Vec) { let query = self.query.trim().to_lowercase(); let aliases: std::collections::BTreeMap<_, _> = p.identities().into_iter().collect(); let identity = |id| { format!( "{} {} {id}", p.named(id), aliases.get(&id).copied().unwrap_or_default() ) }; let mut traces = if self.filter == Filter::Artifacts { vec![] } else { p.traces .values() .filter(|t| { format!( "{} {} {} {} {}", t.name, t.alias, t.description, identity(t.oa), identity(t.sa) ) .to_lowercase() .contains(&query) }) .map(|t| t.id) .collect::>() }; let mut artifacts = if self.filter == Filter::Traces { vec![] } else { p.artifact_links .values() .filter(|a| { format!( "{} {} {} {} {} {} {} {} {:?}", a.name, a.alias, a.description, a.uri, a.revision.as_deref().unwrap_or_default(), a.fragment.as_deref().unwrap_or_default(), identity(a.owner), owner_label(p, a.owner), a.relation ) .to_lowercase() .contains(&query) }) .map(|a| a.id) .collect::>() }; traces.sort_by(|a, b| p.traces[a].name.cmp(&p.traces[b].name).then(a.cmp(b))); artifacts.sort_by(|a, b| { p.artifact_links[a] .name .cmp(&p.artifact_links[b].name) .then(a.cmp(b)) }); (traces, artifacts) } } pub(super) fn owner_label(p: &Project, owner: Id) -> String { p.diagrams .values() .find_map(|d| { d.nodes .get(&owner) .map(|node| format!("Placement of {} ยท {}", p.named(node.element), d.name)) }) .unwrap_or_else(|| p.named(owner).to_owned()) } #[cfg(test)] mod tests { use super::*; use crate::app::{App, Message}; use oa_core::*; fn fixture() -> (Project, Id, [Id; 3]) { let mut p = Project::empty(); let oa = p .add_element(Kind::Activity, Layer::Oa, "Rescue dispatch", None) .unwrap(); let sa = p .add_element(Kind::Function, Layer::Sa, "Dispatch controller", None) .unwrap(); for n in 0..68 { let target = if n == 0 { sa } else { p.add_element( Kind::Function, Layer::Sa, &format!("Dispatch function {n:02}"), None, ) .unwrap() }; let id = Id::new_v4(); p.traces.insert( id, Trace { id, alias: format!("review_{n}"), name: format!("Review {n:02}"), description: String::new(), oa, sa: target, kind: TraceKind::Manual, baseline: None, }, ); } let ids = [Id::from_u128(502), Id::from_u128(501), Id::from_u128(503)]; for (index, id) in ids.iter().copied().enumerate() { p.artifact_links.insert( id, ArtifactLink { id, alias: format!("evidence_{index}"), name: if index == 2 { "Zulu schematic" } else { "Acceptance evidence" } .into(), description: "Engineering evidence".into(), owner: oa, relation: ArtifactRelation::References, uri: format!("./evidence/document_{index}.md"), revision: Some(format!("revision-{index}")), fragment: None, }, ); } validate_candidate(&p).unwrap(); (p, oa, ids) } #[test] fn traceability_filter_reaches_artifacts_after_68_traces_and_searches_all_metadata() { let (p, _, ids) = fixture(); let mut state = State::default(); assert_eq!(state.matching(&p).0.len(), 68); state.update(Event::Filter(Filter::Artifacts)); assert_eq!(state.matching(&p), (vec![], vec![ids[1], ids[0], ids[2]])); for query in ["document_2", "evidence_2", "revision-2", "Zulu schematic"] { state.update(Event::Search(query.into())); assert_eq!(state.matching(&p), (vec![], vec![ids[2]])); } state.update(Event::Search("Rescue dispatch".into())); assert_eq!(state.matching(&p).1.len(), 3); state.update(Event::Filter(Filter::Traces)); assert_eq!(state.matching(&p).0.len(), 68); assert!(state.matching(&p).1.is_empty()); } #[test] fn traceability_browsing_retains_invalid_property_draft_and_existing_select_guard() { let (p, owner, ids) = fixture(); let mut app = App::with_project(p); let _ = app.update(Message::Select(owner)); let _ = app.update(Message::Field("/alias".into(), "invalid alias".into())); let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); let before = app.document.snapshot(); for event in [ Event::Filter(Filter::Artifacts), Event::Search("document_2".into()), Event::Filter(Filter::All), ] { let _ = app.update(Message::Traceability(event)); assert_eq!(app.selected, owner); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &before)); } let _ = app.update(Message::Select(ids[2])); assert_eq!(app.selected, owner); assert_eq!( serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), raw ); assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &before)); } }