Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -519,11 +519,12 @@ mod tests {
|
||||
};
|
||||
state
|
||||
}
|
||||
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::Event) {
|
||||
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::RowEvent) {
|
||||
let event = semantic_state(app).address_event(event);
|
||||
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event)));
|
||||
}
|
||||
fn semantic_decide(app: &mut crate::app::App, facet: FieldFacet, decision: SemanticDecision) {
|
||||
use crate::semantic_review::Event;
|
||||
use crate::semantic_review::RowEvent as Event;
|
||||
semantic_send(app, Event::Select(FacetSelector::Field(facet)));
|
||||
semantic_send(
|
||||
app,
|
||||
@@ -548,7 +549,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep() {
|
||||
use crate::semantic_review::{Event, State};
|
||||
use crate::semantic_review::{RowEvent as Event, State};
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Oa, "Source", None)
|
||||
@@ -610,7 +611,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn independent_review_combined_take_type_informs_actual_default_form_in_both_directions() {
|
||||
use crate::semantic_review::Event;
|
||||
use crate::semantic_review::RowEvent as Event;
|
||||
for reverse in [false, true] {
|
||||
for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] {
|
||||
let mut p = Project::empty();
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
//! Closed view intent and named category membership through ordinary property drafts.
|
||||
use super::*;
|
||||
use iced::widget::column;
|
||||
use oa_core::view_options::{
|
||||
self, CategorySelection, ContextSelection, Coverage, EdgeAvailability, EdgeKind,
|
||||
ExchangeFamily, ExchangeLabels, OmissionReason, RelationKinds, ViewIndex, ViewOptions,
|
||||
};
|
||||
|
||||
const PAGE_SIZE: usize = 24;
|
||||
|
||||
struct Candidate {
|
||||
id: Id,
|
||||
label: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
fn choice<'a>(
|
||||
label: &str,
|
||||
path: &str,
|
||||
selected: &'static str,
|
||||
choices: Vec<(&'static str, &'static str)>,
|
||||
) -> View<'a, Message> {
|
||||
let path = path.to_owned();
|
||||
let selected = choices
|
||||
.iter()
|
||||
.find(|(_, value)| *value == selected)
|
||||
.map(|(label, _)| *label);
|
||||
let labels: Vec<_> = choices.iter().map(|(label, _)| *label).collect();
|
||||
column![
|
||||
text(label.to_owned()).size(12).color(style::MUTED),
|
||||
pick_list(labels, selected, move |label| Message::FieldShape(
|
||||
path.clone(),
|
||||
json!(
|
||||
choices
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == label)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
))
|
||||
.text_size(12)
|
||||
.padding(7)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(5)
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn named(project: &Project, id: Id, detail: impl Into<String>) -> Candidate {
|
||||
let alias = project
|
||||
.elements
|
||||
.get(&id)
|
||||
.map(|element| element.alias.as_str())
|
||||
.or_else(|| {
|
||||
project
|
||||
.relations
|
||||
.get(&id)
|
||||
.map(|relation| relation.alias.as_str())
|
||||
})
|
||||
.or_else(|| project.traces.get(&id).map(|trace| trace.alias.as_str()))
|
||||
.unwrap_or_default();
|
||||
Candidate {
|
||||
id,
|
||||
label: format!("{} · {}", project.named(id), alias),
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_value(ids: &[Id], wrapper: Option<&str>) -> Value {
|
||||
match wrapper {
|
||||
Some(wrapper) => json!({wrapper: ids}),
|
||||
None => json!(ids),
|
||||
}
|
||||
}
|
||||
|
||||
fn members<'a>(
|
||||
draft: &'a Draft,
|
||||
path: &str,
|
||||
selected: &[Id],
|
||||
mut candidates: Vec<Candidate>,
|
||||
wrapper: Option<&'static str>,
|
||||
verb: &'static str,
|
||||
) -> View<'a, Message> {
|
||||
// Preserve and expose now-ineligible identities until the engineer removes them.
|
||||
for id in selected {
|
||||
if !candidates.iter().any(|candidate| candidate.id == *id) {
|
||||
candidates.push(Candidate {
|
||||
id: *id,
|
||||
label: format!("Retained reference · {id}"),
|
||||
detail:
|
||||
"Missing or outside the chosen layer/family. Remove or correct before Apply."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
candidates.sort_by_key(|candidate| (candidate.label.to_lowercase(), candidate.id));
|
||||
let browser = draft.reference_browsers.get(path);
|
||||
let query = browser.map(|browser| browser.query.as_str()).unwrap_or("");
|
||||
let search = query.to_lowercase();
|
||||
candidates.retain(|candidate| {
|
||||
format!("{} {} {}", candidate.label, candidate.id, candidate.detail)
|
||||
.to_lowercase()
|
||||
.contains(&search)
|
||||
});
|
||||
let page = browser
|
||||
.map(|browser| browser.page)
|
||||
.unwrap_or(0)
|
||||
.min(candidates.len().saturating_sub(1) / PAGE_SIZE);
|
||||
let start = page * PAGE_SIZE;
|
||||
let path_owned = path.to_owned();
|
||||
let mut out = column![
|
||||
text_input("Find by name, alias, identity or reason", query)
|
||||
.on_input(move |query| Message::FieldReferenceBrowse(path_owned.clone(), query, 0))
|
||||
.size(12)
|
||||
.padding(7),
|
||||
text(format!(
|
||||
"{} {} · {} matching",
|
||||
selected.len(),
|
||||
if verb == "Hide" {
|
||||
"explicitly hidden"
|
||||
} else {
|
||||
"selected"
|
||||
},
|
||||
candidates.len()
|
||||
))
|
||||
.size(11)
|
||||
.color(style::MUTED)
|
||||
]
|
||||
.spacing(6)
|
||||
.width(Fill);
|
||||
if candidates.is_empty() {
|
||||
out = out.push(text("No matching records.").size(12));
|
||||
}
|
||||
for candidate in candidates.iter().skip(start).take(PAGE_SIZE) {
|
||||
let checked = selected.contains(&candidate.id);
|
||||
let id = candidate.id;
|
||||
let selected = selected.to_vec();
|
||||
let path = path.to_owned();
|
||||
out = out.push(
|
||||
column![
|
||||
checkbox(checked)
|
||||
.label(format!("{verb} {}", candidate.label))
|
||||
.size(14)
|
||||
.text_size(12)
|
||||
.on_toggle(move |enabled| {
|
||||
let mut ids = selected.clone();
|
||||
ids.retain(|existing| *existing != id);
|
||||
if enabled {
|
||||
ids.push(id);
|
||||
}
|
||||
Message::FieldShape(path.clone(), list_value(&ids, wrapper))
|
||||
}),
|
||||
text(candidate.detail.clone()).size(11).color(style::MUTED)
|
||||
]
|
||||
.spacing(3)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
if candidates.len() > PAGE_SIZE {
|
||||
let mut previous = button(text("Previous").size(11)).style(style::nav);
|
||||
if page > 0 {
|
||||
previous = previous.on_press(Message::FieldReferenceBrowse(
|
||||
path.into(),
|
||||
query.into(),
|
||||
page - 1,
|
||||
));
|
||||
}
|
||||
let mut next = button(text("Next").size(11)).style(style::nav);
|
||||
if start + PAGE_SIZE < candidates.len() {
|
||||
next = next.on_press(Message::FieldReferenceBrowse(
|
||||
path.into(),
|
||||
query.into(),
|
||||
page + 1,
|
||||
));
|
||||
}
|
||||
out = out.push(
|
||||
row![
|
||||
previous,
|
||||
text(format!(
|
||||
"Page {} of {}",
|
||||
page + 1,
|
||||
candidates.len().div_ceil(PAGE_SIZE)
|
||||
))
|
||||
.size(11),
|
||||
next
|
||||
]
|
||||
.spacing(6),
|
||||
);
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
|
||||
fn categories<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &Project,
|
||||
layer: Layer,
|
||||
path: &str,
|
||||
family: ExchangeFamily,
|
||||
selection: &CategorySelection,
|
||||
cross_layer: bool,
|
||||
) -> View<'a, Message> {
|
||||
let label = match family {
|
||||
ExchangeFamily::Work => "Work exchange categories",
|
||||
ExchangeFamily::Participant => "Participant exchange categories",
|
||||
};
|
||||
let all = matches!(selection, CategorySelection::All);
|
||||
let path_owned = path.to_owned();
|
||||
let mut out = column![
|
||||
text(label).size(13),
|
||||
checkbox(all)
|
||||
.label("All, including uncategorized exchanges")
|
||||
.text_size(12)
|
||||
.on_toggle(move |all| Message::FieldShape(
|
||||
path_owned.clone(),
|
||||
if all {
|
||||
json!(CategorySelection::All)
|
||||
} else {
|
||||
json!(CategorySelection::Any(vec![]))
|
||||
}
|
||||
))
|
||||
]
|
||||
.spacing(6)
|
||||
.width(Fill);
|
||||
if let CategorySelection::Any(selected) = selection {
|
||||
out = out.push(
|
||||
text("Selected categories combine by union. Selecting none shows none of this family.")
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
let candidates = project
|
||||
.elements
|
||||
.values()
|
||||
.filter_map(|element| {
|
||||
let SharedBody::ExchangeCategory {
|
||||
family: candidate_family,
|
||||
exchanges,
|
||||
} = element.shared()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
((cross_layer || element.layer() == layer) && *candidate_family == family).then(
|
||||
|| {
|
||||
named(
|
||||
project,
|
||||
element.id,
|
||||
format!("{} exchange members", exchanges.len()),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
out = out.push(members(
|
||||
draft,
|
||||
path,
|
||||
selected,
|
||||
candidates,
|
||||
Some("Any"),
|
||||
"Include",
|
||||
));
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
|
||||
fn reason(reason: OmissionReason) -> &'static str {
|
||||
match reason {
|
||||
OmissionReason::ExplicitHide => "explicitly hidden",
|
||||
OmissionReason::RelationKind => "relationship kind excluded",
|
||||
OmissionReason::Category(ExchangeFamily::Work) => "outside selected work categories",
|
||||
OmissionReason::Category(ExchangeFamily::Participant) => {
|
||||
"outside selected participant categories"
|
||||
}
|
||||
OmissionReason::Coverage => "outside selected global behavior coverage",
|
||||
OmissionReason::WorkContext => "outside selected process, chain or scenario",
|
||||
OmissionReason::OutsideProjection => "outside this projection",
|
||||
OmissionReason::EndpointNotShown {
|
||||
source: true,
|
||||
target: true,
|
||||
} => "source and target are not shown",
|
||||
OmissionReason::EndpointNotShown {
|
||||
source: true,
|
||||
target: false,
|
||||
} => "source is not shown",
|
||||
OmissionReason::EndpointNotShown {
|
||||
source: false,
|
||||
target: true,
|
||||
} => "target is not shown",
|
||||
OmissionReason::EndpointNotShown {
|
||||
source: false,
|
||||
target: false,
|
||||
} => "endpoint placement unavailable",
|
||||
OmissionReason::FoldedInternal => "internal to a collapsed or grouped node",
|
||||
OmissionReason::WrongLayer => "outside this layer",
|
||||
OmissionReason::UnknownEdge => "edge no longer exists",
|
||||
}
|
||||
}
|
||||
|
||||
fn options<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &Project,
|
||||
path: &str,
|
||||
options: ViewOptions,
|
||||
) -> View<'a, Message> {
|
||||
let diagram = serde_json::from_value::<Diagram>(draft.context_value())
|
||||
.ok()
|
||||
.or_else(|| project.diagrams.get(&draft.original).cloned());
|
||||
let Some(diagram) = diagram else {
|
||||
return text("View options require a saved view.").size(12).into();
|
||||
};
|
||||
let layer = diagram.layer;
|
||||
let cross_layer = diagram.projection == ProjectionKind::Traceability;
|
||||
let kinds_path = format!("{path}/relation_kinds");
|
||||
let all_kinds = matches!(options.relation_kinds, RelationKinds::All);
|
||||
let toggle_path = kinds_path.clone();
|
||||
let mut out = column![text("Saved view visibility").size(15),
|
||||
text("Apply changes to this view. Model relationships, nesting and saved routes remain available.").size(11).color(style::MUTED),
|
||||
choice("Exchange labels", &format!("{path}/exchange_labels"), match options.exchange_labels { ExchangeLabels::Name => "Name", ExchangeLabels::Items => "Items", ExchangeLabels::NameAndItems => "NameAndItems" }, vec![("Names", "Name"), ("Items", "Items"), ("Names and items", "NameAndItems")]),
|
||||
checkbox(all_kinds).label("All relationship line kinds").text_size(12).on_toggle(move |all| Message::FieldShape(toggle_path.clone(), if all { json!(RelationKinds::All) } else { json!(RelationKinds::Only(vec![])) }))].spacing(10).width(Fill);
|
||||
if let RelationKinds::Only(selected) = &options.relation_kinds {
|
||||
out = out.push(text("No selected kinds means no relationship lines. Nesting and state action rows remain.").size(11).color(style::MUTED));
|
||||
for kind in RelationKind::ALL
|
||||
.into_iter()
|
||||
.map(EdgeKind::Relation)
|
||||
.chain([EdgeKind::Trace])
|
||||
{
|
||||
let label = match kind {
|
||||
EdgeKind::Relation(kind) => kind.to_string(),
|
||||
EdgeKind::Trace => "Realization traces".into(),
|
||||
};
|
||||
let checked = selected.contains(&kind);
|
||||
let selected = selected.clone();
|
||||
let path = kinds_path.clone();
|
||||
out = out.push(checkbox(checked).label(label).text_size(12).on_toggle(
|
||||
move |enabled| {
|
||||
let mut kinds = selected.clone();
|
||||
kinds.retain(|existing| *existing != kind);
|
||||
if enabled {
|
||||
kinds.push(kind);
|
||||
}
|
||||
Message::FieldShape(path.clone(), json!(RelationKinds::Only(kinds)))
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
out = out.push(categories(draft, project, layer, &format!("{path}/work_categories"), ExchangeFamily::Work, &options.work_categories, cross_layer))
|
||||
.push(categories(draft, project, layer, &format!("{path}/participant_categories"), ExchangeFamily::Participant, &options.participant_categories, cross_layer))
|
||||
.push(choice("Work exchange coverage across the whole model", &format!("{path}/coverage"), match options.coverage { Coverage::All => "All", Coverage::InBehavior => "InBehavior", Coverage::OutsideBehavior => "OutsideBehavior" }, vec![("All work exchanges", "All"), ("Used in behavior", "InBehavior"), ("Outside behavior", "OutsideBehavior")]))
|
||||
.push(text("Coverage uses explicit exchange occurrences in processes, chains and scenarios, including reuse. It does not infer execution or participant transport coverage.").size(11).color(style::MUTED));
|
||||
let context_path = format!("{path}/work_contexts");
|
||||
let toggle_path = context_path.clone();
|
||||
out = out.push(
|
||||
checkbox(matches!(options.work_contexts, ContextSelection::All))
|
||||
.label("All work contexts")
|
||||
.text_size(12)
|
||||
.on_toggle(move |all| {
|
||||
Message::FieldShape(
|
||||
toggle_path.clone(),
|
||||
if all {
|
||||
json!(ContextSelection::All)
|
||||
} else {
|
||||
json!(ContextSelection::Any(vec![]))
|
||||
},
|
||||
)
|
||||
}),
|
||||
);
|
||||
if let ContextSelection::Any(selected) = &options.work_contexts {
|
||||
let candidates = project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|element| {
|
||||
(cross_layer || element.layer() == layer)
|
||||
&& matches!(
|
||||
element.kind(),
|
||||
Kind::Process | Kind::FunctionalChain | Kind::Scenario
|
||||
)
|
||||
})
|
||||
.map(|element| named(project, element.id, element.kind().label()))
|
||||
.collect();
|
||||
out = out.push(text("Show work exchanges used by any selected context, intersected with the coverage and category choices above. None selected shows no work exchanges.").size(11).color(style::MUTED))
|
||||
.push(members(draft, &context_path, selected, candidates, Some("Any"), "Include"));
|
||||
}
|
||||
// Both expensive indexes are built once for the panel, never once per edge.
|
||||
let index = ViewIndex::new(project);
|
||||
let filter = if cross_layer {
|
||||
index.prepare_cross_layer(layer, &options)
|
||||
} else {
|
||||
index.prepare(layer, &options)
|
||||
};
|
||||
let availability = projection::edge_availability(project, &diagram);
|
||||
let mut shown = 0usize;
|
||||
let candidates: Vec<_> = project
|
||||
.relations
|
||||
.keys()
|
||||
.chain(project.traces.keys())
|
||||
.filter(|id| {
|
||||
cross_layer || project.layer_of(**id) == Some(layer) || project.traces.contains_key(id)
|
||||
})
|
||||
.map(|id| {
|
||||
let reasons = filter.reasons(
|
||||
*id,
|
||||
availability
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(EdgeAvailability::OutsideProjection),
|
||||
);
|
||||
shown += usize::from(reasons.is_empty());
|
||||
named(
|
||||
project,
|
||||
*id,
|
||||
if reasons.is_empty() {
|
||||
"Shown by these settings".into()
|
||||
} else {
|
||||
reasons
|
||||
.into_iter()
|
||||
.map(reason)
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
out = out.push(text("Individual lines and omission reasons").size(13))
|
||||
.push(text(format!("With these settings: {shown} graphical lines shown · {} lines omitted", candidates.len() - shown)).size(11).color(style::MUTED))
|
||||
.push(text("Check Hide to retain an explicit hide. Unchecking removes only that hide; other listed reasons can still omit the line.").size(11).color(style::MUTED))
|
||||
.push(button(text("Clear all explicit hides").size(11)).style(style::nav).on_press(Message::FieldShape(format!("{path}/hidden_edges"), json!([]))))
|
||||
.push(members(draft, &format!("{path}/hidden_edges"), &options.hidden_edges, candidates, None, "Hide"));
|
||||
out.into()
|
||||
}
|
||||
|
||||
fn category_members<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &Project,
|
||||
path: &str,
|
||||
value: &Value,
|
||||
) -> View<'a, Message> {
|
||||
let context = draft.context_value();
|
||||
let parent = path
|
||||
.rsplit_once('/')
|
||||
.map(|(parent, _)| parent)
|
||||
.unwrap_or_default();
|
||||
let family = context
|
||||
.pointer(&format!("{parent}/family"))
|
||||
.and_then(|value| serde_json::from_value::<ExchangeFamily>(value.clone()).ok())
|
||||
.unwrap_or(ExchangeFamily::Work);
|
||||
let layer = project.layer_of(draft.original).unwrap_or(Layer::Oa);
|
||||
let Ok(selected) = serde_json::from_value::<Vec<Id>>(value.clone()) else {
|
||||
return column![text("The retained member list contains an invalid reference. Clear it explicitly or cancel this property draft before choosing members.").size(12),
|
||||
button(text("Clear invalid member list").size(11)).style(style::nav).on_press(Message::FieldShape(path.into(), json!([])))].spacing(6).width(Fill).into();
|
||||
};
|
||||
let candidates = project
|
||||
.relations
|
||||
.values()
|
||||
.filter(|relation| {
|
||||
view_options::validate_category(project, layer, None, family, &[relation.id]).is_empty()
|
||||
})
|
||||
.map(|relation| {
|
||||
named(
|
||||
project,
|
||||
relation.id,
|
||||
format!(
|
||||
"{} → {} · {}",
|
||||
project.named(relation.source()),
|
||||
project.named(relation.target()),
|
||||
relation.kind()
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
column![text("Exchange members").size(13), text("Membership is a reusable concern. It does not allocate exchanges, change endpoints or imply execution. An empty category is valid.").size(11).color(style::MUTED),
|
||||
members(draft, path, &selected, candidates, None, "Include")].spacing(6).width(Fill).into()
|
||||
}
|
||||
|
||||
pub(super) fn property<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &'a Project,
|
||||
value: &'a Value,
|
||||
path: &str,
|
||||
) -> Option<View<'a, Message>> {
|
||||
if path == "/view_options" {
|
||||
return Some(match serde_json::from_value::<ViewOptions>(value.clone()) {
|
||||
Ok(value) => options(draft, project, path, value),
|
||||
Err(error) => text(format!("View settings are invalid: {error}. The draft remains available for correction in source.")).size(12).into(),
|
||||
});
|
||||
}
|
||||
if path.ends_with("/ExchangeCategory") {
|
||||
return Some(
|
||||
column![
|
||||
choice(
|
||||
"Exchange family",
|
||||
&format!("{path}/family"),
|
||||
if value.get("family").and_then(Value::as_str) == Some("Participant") {
|
||||
"Participant"
|
||||
} else {
|
||||
"Work"
|
||||
},
|
||||
vec![
|
||||
("Work exchanges", "Work"),
|
||||
("Participant exchanges", "Participant")
|
||||
]
|
||||
),
|
||||
category_members(
|
||||
draft,
|
||||
project,
|
||||
&format!("{path}/exchanges"),
|
||||
value.get("exchanges").unwrap_or(&Value::Null)
|
||||
)
|
||||
]
|
||||
.spacing(10)
|
||||
.width(Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if path.contains("/ExchangeCategory/") {
|
||||
if path.ends_with("/family") {
|
||||
return Some(choice(
|
||||
"Exchange family",
|
||||
path,
|
||||
if value.as_str() == Some("Participant") {
|
||||
"Participant"
|
||||
} else {
|
||||
"Work"
|
||||
},
|
||||
vec![
|
||||
("Work exchanges", "Work"),
|
||||
("Participant exchanges", "Participant"),
|
||||
],
|
||||
));
|
||||
}
|
||||
if path.ends_with("/exchanges") {
|
||||
return Some(category_members(draft, project, path, value));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn selective_view_property_apply_recovery_and_undo_preserve_semantics_and_empty_choices() {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Request", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Provide", None)
|
||||
.unwrap();
|
||||
let exchange = project.relate(RelationKind::Interaction, "Request data", a, b);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.place(diagram, a, Position { x: 10., y: 20. });
|
||||
project.place(diagram, b, Position { x: 300., y: 20. });
|
||||
let mut draft = Draft::from_project(&project, diagram).unwrap();
|
||||
draft
|
||||
.shape(
|
||||
"/view_options/work_categories",
|
||||
list_value(&[], Some("Any")),
|
||||
)
|
||||
.unwrap();
|
||||
draft
|
||||
.shape("/view_options/hidden_edges", list_value(&[exchange], None))
|
||||
.unwrap();
|
||||
draft
|
||||
.shape("/view_options/exchange_labels", json!("NameAndItems"))
|
||||
.unwrap();
|
||||
draft.reference_browsers.insert(
|
||||
"/view_options/hidden_edges".into(),
|
||||
ReferenceBrowser {
|
||||
query: "Request".into(),
|
||||
page: 3,
|
||||
},
|
||||
);
|
||||
let restored = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
assert_eq!(
|
||||
restored.materialize().unwrap(),
|
||||
draft.materialize().unwrap()
|
||||
);
|
||||
assert!(restored.reference_browsers.is_empty());
|
||||
let mut session = Session::new(project.clone());
|
||||
session.execute(restored.command().unwrap()).unwrap();
|
||||
let accepted = session.project();
|
||||
assert_eq!(accepted.elements, project.elements);
|
||||
assert_eq!(accepted.relations, project.relations);
|
||||
assert_eq!(
|
||||
accepted.diagrams[&diagram].nodes,
|
||||
project.diagrams[&diagram].nodes
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.diagrams[&diagram].view_options.work_categories,
|
||||
CategorySelection::Any(vec![])
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.diagrams[&diagram].view_options.hidden_edges,
|
||||
vec![exchange]
|
||||
);
|
||||
assert_eq!(
|
||||
language::parse(&language::format(accepted)).unwrap(),
|
||||
*accepted
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_family_change_retains_members_until_explicit_correction() {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Request", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Provide", None)
|
||||
.unwrap();
|
||||
let exchange = project.relate(RelationKind::Interaction, "Request data", a, b);
|
||||
let id = project
|
||||
.add_element(Kind::ExchangeCategory, Layer::Oa, "Service concern", None)
|
||||
.unwrap();
|
||||
*project.elements.get_mut(&id).unwrap().shared_mut().unwrap() =
|
||||
SharedBody::ExchangeCategory {
|
||||
family: ExchangeFamily::Work,
|
||||
exchanges: vec![exchange],
|
||||
};
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
let path = "/body/Oa/Shared/ExchangeCategory";
|
||||
draft
|
||||
.shape(&format!("{path}/family"), json!("Participant"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft
|
||||
.materialize()
|
||||
.unwrap()
|
||||
.pointer(&format!("{path}/exchanges")),
|
||||
Some(&json!([exchange]))
|
||||
);
|
||||
assert!(execute_batch(&project, &[draft.command().unwrap()]).is_err());
|
||||
let mut restored = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
restored
|
||||
.shape(&format!("{path}/exchanges"), list_value(&[], None))
|
||||
.unwrap();
|
||||
let accepted = execute_batch(&project, &[restored.command().unwrap()]).unwrap();
|
||||
assert!(
|
||||
matches!(accepted.elements[&id].shared(), Some(SharedBody::ExchangeCategory { family: ExchangeFamily::Participant, exchanges }) if exchanges.is_empty())
|
||||
);
|
||||
assert_eq!(accepted.relations, project.relations);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user