Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17f1f89fef | ||
|
|
4fbdf6f385 | ||
|
|
bfc259854f |
@@ -22,6 +22,8 @@ pub(crate) mod source_rename;
|
||||
mod source_symbols;
|
||||
pub(crate) mod text_workspace;
|
||||
#[cfg(test)]
|
||||
mod traceability_navigation_tests;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
mod view_copy_tests;
|
||||
@@ -359,6 +361,7 @@ pub enum Message {
|
||||
Page(Page),
|
||||
Guide(crate::guide::Event),
|
||||
Traceability(crate::views::traceability::Event),
|
||||
ReviewLayerTraces,
|
||||
Behavior(crate::behavior::Event),
|
||||
Layer(Layer),
|
||||
Select(Id),
|
||||
@@ -2310,7 +2313,30 @@ impl App {
|
||||
"Apply or revert pending edits before using modeling actions.".into();
|
||||
return Task::none();
|
||||
}
|
||||
// Show acts on the diagram captured by the validated menu.
|
||||
// Ordinary cross-layer selection would switch to that element's
|
||||
// preferred same-layer view before the placement command runs.
|
||||
let show_in_mixed_view = matches!(&*action, Message::PlaceSelected)
|
||||
&& self
|
||||
.active_diagram
|
||||
.and_then(|id| self.project().diagrams.get(&id))
|
||||
.is_some_and(|d| {
|
||||
d.projection == ProjectionKind::Traceability
|
||||
&& self
|
||||
.project()
|
||||
.elements
|
||||
.get(&menu.request.object)
|
||||
.is_some_and(|e| {
|
||||
e.kind() != Kind::Port && d.projection.includes(e.kind())
|
||||
})
|
||||
});
|
||||
if show_in_mixed_view {
|
||||
self.selected = menu.request.object;
|
||||
self.selected_attachment = None;
|
||||
self.refresh_draft();
|
||||
} else {
|
||||
self.select(menu.request.object);
|
||||
}
|
||||
self.selected_node = menu.request.placement;
|
||||
self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i));
|
||||
return self.update(*action);
|
||||
@@ -2508,6 +2534,23 @@ impl App {
|
||||
iced::widget::scrollable::RelativeOffset::START,
|
||||
);
|
||||
}
|
||||
Message::ReviewLayerTraces => {
|
||||
// Reuse Page navigation so modal, placement and behavior-form
|
||||
// guards run before changing the transient inventory filter.
|
||||
let navigation = self.update(Message::Page(Page::Traces));
|
||||
if self.page == Page::Traces && !self.behavior.has_pending() {
|
||||
self.traceability.filter = crate::views::traceability::Filter::Traces;
|
||||
self.traceability.query.clear();
|
||||
return Task::batch([
|
||||
navigation,
|
||||
iced::widget::operation::snap_to(
|
||||
"traceability-list",
|
||||
iced::widget::scrollable::RelativeOffset::START,
|
||||
),
|
||||
]);
|
||||
}
|
||||
return navigation;
|
||||
}
|
||||
Message::Search(value) => {
|
||||
self.search = value;
|
||||
self.explorer_page = 0;
|
||||
|
||||
@@ -301,6 +301,17 @@ impl App {
|
||||
_ => return Task::none(),
|
||||
};
|
||||
let placing = point.is_none() && matches!(intent, Intent::Create { .. });
|
||||
let filter = if matches!(intent, Intent::Existing { .. })
|
||||
&& self
|
||||
.project()
|
||||
.diagrams
|
||||
.get(&diagram)
|
||||
.is_some_and(|d| d.projection == ProjectionKind::Traceability)
|
||||
{
|
||||
Filter::AllCompatible
|
||||
} else {
|
||||
Filter::Context
|
||||
};
|
||||
let saved = Saved {
|
||||
version: 1,
|
||||
base: oa_core::transitions::project_fingerprint(self.project()),
|
||||
@@ -309,7 +320,7 @@ impl App {
|
||||
point,
|
||||
placing,
|
||||
query: String::new(),
|
||||
filter: Filter::Context,
|
||||
filter,
|
||||
show_placed: false,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
mod traceability;
|
||||
|
||||
fn setup(layer: Layer) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn traceability_existing_defaults_show_opposite_layer_and_place_only_one_node() {
|
||||
let mut project = Project::empty();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Deliver report", None)
|
||||
.unwrap();
|
||||
let system = project
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let mut app = App::with_project(project);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
let before = app.document.snapshot();
|
||||
send(&mut app, Event::Existing(None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::AllCompatible
|
||||
);
|
||||
let listed = candidates(&app);
|
||||
assert!(listed.iter().any(|(id, _, shown)| *id == sa && !shown));
|
||||
assert!(listed.iter().any(|(id, _, shown)| *id == system && !shown));
|
||||
send(&mut app, Event::Pick(sa));
|
||||
assert!(app.architecture.as_ref().unwrap().saved.placing);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
let context = app.route_context().unwrap();
|
||||
let point = Position { x: 345., y: 215. };
|
||||
send(&mut app, Event::Click(context, point));
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
let placed = app.project().diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == sa)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(placed.position, point);
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(placed.id, placed);
|
||||
assert_eq!(app.project(), &expected);
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traceability_default_does_not_change_creation_or_ordinary_picker_guards() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let diagram = app.active_diagram.unwrap();
|
||||
send(&mut app, Event::Existing(None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::Context
|
||||
);
|
||||
send(&mut app, Event::Cancel);
|
||||
let _ = app.update(Message::Projection(ProjectionKind::Traceability));
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::Context
|
||||
);
|
||||
send(&mut app, Event::Cancel);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
|
||||
let mut project = app.project().clone();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Other layer", None)
|
||||
.unwrap();
|
||||
let port = Id::new_v4();
|
||||
project.elements.insert(
|
||||
port,
|
||||
oa_core::Element {
|
||||
id: port,
|
||||
alias: "legacy_port".into(),
|
||||
name: "Port".into(),
|
||||
description: String::new(),
|
||||
parent: Some(sa),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
|
||||
let mut p = project.clone();
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
||||
let mut trial = App::with_project(p);
|
||||
trial.page = Page::Diagram;
|
||||
trial.active_diagram = Some(diagram);
|
||||
trial.layer = Layer::Oa;
|
||||
let before = trial.document.bundle().clone();
|
||||
send(&mut trial, Event::Existing(None));
|
||||
send(&mut trial, Event::Filter(Filter::AllCompatible));
|
||||
assert_eq!(
|
||||
candidates(&trial).iter().any(|(id, _, _)| *id == sa),
|
||||
projection == ProjectionKind::Traceability
|
||||
);
|
||||
assert!(!candidates(&trial).iter().any(|(id, _, _)| *id == port));
|
||||
send(&mut trial, Event::Pick(port));
|
||||
assert!(!trial.architecture.as_ref().unwrap().saved.placing);
|
||||
send(&mut trial, Event::Cancel);
|
||||
assert_eq!(trial.document.bundle(), &before);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
use super::*;
|
||||
use crate::{context_menu::Entry, model_menu, views::traceability::Filter};
|
||||
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
fn fixture() -> (App, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
||||
.unwrap();
|
||||
let mixed = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
||||
p.place(mixed, oa, Position { x: 300., y: 200. });
|
||||
let system = p.ensure_diagram(Layer::Sa);
|
||||
p.place(system, sa, Position { x: 700., y: 200. });
|
||||
oa_core::validate_candidate(&p).unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(mixed);
|
||||
app.selected = oa;
|
||||
app.refresh_draft();
|
||||
app.traceability.filter = Filter::Artifacts;
|
||||
app.traceability.query = "stale artifact query".into();
|
||||
(app, mixed, system, oa, sa)
|
||||
}
|
||||
|
||||
fn show_action(app: &mut App, element: Id) -> Message {
|
||||
send(app, model_menu::open(element, iced::Point::ORIGIN));
|
||||
model_menu::popup(app)
|
||||
.unwrap()
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|entry| match entry {
|
||||
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
|
||||
Some(message)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("eligible real menu action")
|
||||
.expect("eligible menu action must be enabled")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_layer_traces_resets_inventory_without_applying_property_or_source_drafts() {
|
||||
for source_draft in [false, true] {
|
||||
let (mut app, mixed, _, oa, _) = fixture();
|
||||
if source_draft {
|
||||
send(&mut app, Message::Page(Page::Text));
|
||||
send(
|
||||
&mut app,
|
||||
Message::SourceEdit(iced::widget::text_editor::Action::Edit(
|
||||
iced::widget::text_editor::Edit::Insert('x'),
|
||||
)),
|
||||
);
|
||||
assert!(app.source_dirty());
|
||||
} else {
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/alias".into(), "invalid alias".into()),
|
||||
);
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
let source = app.source_editor.text();
|
||||
let before = app.document.snapshot();
|
||||
let camera = (app.zoom, app.pan);
|
||||
send(&mut app, Message::ReviewLayerTraces);
|
||||
assert_eq!(app.page, Page::Traces);
|
||||
assert_eq!(app.traceability.filter, Filter::Traces);
|
||||
assert!(app.traceability.query.is_empty());
|
||||
assert_eq!(
|
||||
(app.layer, app.active_diagram, app.selected),
|
||||
(Layer::Oa, Some(mixed), oa)
|
||||
);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
assert_eq!(
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
raw
|
||||
);
|
||||
assert_eq!(app.source_editor.text(), source);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_layer_traces_respects_busy_modal_and_active_placement_guards() {
|
||||
for already_traces in [false, true] {
|
||||
for guard in 0..5 {
|
||||
let (mut app, mixed, _, oa, _) = fixture();
|
||||
match guard {
|
||||
0 => app.busy = true,
|
||||
1 => app.delete_pending = true,
|
||||
2 => app.hosted.visible = true,
|
||||
3 => send(
|
||||
&mut app,
|
||||
Message::Architecture(architecture_authoring::Event::Existing(None)),
|
||||
),
|
||||
4 => {
|
||||
app.page = Page::Behavior;
|
||||
let p = app.project().clone();
|
||||
app.behavior
|
||||
.creation
|
||||
.update(&p, behavior::creation::Event::Open(Layer::Oa))
|
||||
.unwrap();
|
||||
assert!(app.behavior.has_pending());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if already_traces {
|
||||
app.page = Page::Traces;
|
||||
}
|
||||
let original_page = app.page;
|
||||
let before = app.document.snapshot();
|
||||
let behavior_before = app.behavior.recovery_snapshot().unwrap();
|
||||
send(&mut app, Message::ReviewLayerTraces);
|
||||
assert_eq!(
|
||||
app.page, original_page,
|
||||
"guard {guard}, already traces {already_traces}"
|
||||
);
|
||||
assert_eq!(app.traceability.filter, Filter::Artifacts);
|
||||
assert_eq!(app.traceability.query, "stale artifact query");
|
||||
assert_eq!(
|
||||
(app.layer, app.active_diagram, app.selected),
|
||||
(Layer::Oa, Some(mixed), oa)
|
||||
);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior_before);
|
||||
if guard == 3 {
|
||||
assert!(app.architecture.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_keeps_its_context_for_every_diagram_app_and_target_layer() {
|
||||
for diagram_layer in [Layer::Oa, Layer::Sa] {
|
||||
for app_layer in [Layer::Oa, Layer::Sa] {
|
||||
for target_layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
||||
.unwrap();
|
||||
let target = if target_layer == Layer::Oa { oa } else { sa };
|
||||
let other = if target_layer == Layer::Oa { sa } else { oa };
|
||||
let mixed = p.ensure_diagram(diagram_layer);
|
||||
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
||||
// An attractive same-layer alternative makes accidental
|
||||
// semantic-selection navigation observable in both directions.
|
||||
let mut alternative = p.diagrams[&mixed].clone();
|
||||
alternative.id = Id::new_v4();
|
||||
alternative.alias = "same_layer_alternative".into();
|
||||
alternative.layer = target_layer;
|
||||
alternative.projection = ProjectionKind::Overview;
|
||||
let alternative_id = alternative.id;
|
||||
p.diagrams.insert(alternative_id, alternative);
|
||||
p.place(alternative_id, target, Position { x: 700., y: 200. });
|
||||
oa_core::validate_candidate(&p).unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = app_layer;
|
||||
app.active_diagram = Some(mixed);
|
||||
app.selected = other;
|
||||
app.refresh_draft();
|
||||
let before = app.project().clone();
|
||||
let action = show_action(&mut app, target);
|
||||
send(&mut app, action);
|
||||
// The existing RevealInDiagram action synchronizes the layer
|
||||
// to the captured view, even if the prior perspective differed.
|
||||
assert_eq!(
|
||||
(app.active_diagram, app.layer, app.selected),
|
||||
(Some(mixed), diagram_layer, target)
|
||||
);
|
||||
let added = app.project().diagrams[&mixed]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == target)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(app.selected_node, Some(added.id));
|
||||
let mut expected = before;
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&mixed)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(added.id, added);
|
||||
assert_eq!(app.project(), &expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_menu_places_in_captured_view_with_exact_undo_redo() {
|
||||
let (mut app, mixed, system, _, sa) = fixture();
|
||||
let before = app.document.snapshot();
|
||||
let action = show_action(&mut app, sa);
|
||||
send(&mut app, action);
|
||||
assert_eq!(
|
||||
(app.active_diagram, app.layer, app.selected),
|
||||
(Some(mixed), Layer::Oa, sa)
|
||||
);
|
||||
let added = app.project().diagrams[&mixed]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == sa)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(added.position, Position { x: 60., y: 60. });
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&mixed)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(added.id, added);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&system],
|
||||
before.decoded.project.diagrams[&system]
|
||||
);
|
||||
let after = app.document.snapshot();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &after.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &after.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_menu_rejects_stale_context_and_pending_property_changes() {
|
||||
for refusal in 0..3 {
|
||||
let (mut app, mixed, system, oa, sa) = fixture();
|
||||
let action = show_action(&mut app, sa);
|
||||
match refusal {
|
||||
0 => app.active_diagram = Some(system),
|
||||
1 => app.canvas_epoch = app.canvas_epoch.wrapping_add(1),
|
||||
2 => {
|
||||
// A queued action must not apply a draft that became dirty
|
||||
// after the menu was rendered, even when its context is valid.
|
||||
app.draft
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.input("/name", "Unapplied name".into());
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let before = app.document.snapshot();
|
||||
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
send(&mut app, action);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.selected, oa);
|
||||
assert_eq!(
|
||||
app.active_diagram,
|
||||
Some(if refusal == 0 { system } else { mixed })
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
raw
|
||||
);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,7 @@ fn article(topic: Topic) -> Article {
|
||||
"Choose and edit the relationship you mean",
|
||||
&[
|
||||
"Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective, projection and Saved view visibility. Apply changes commits the view settings.",
|
||||
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships is a broader exploration view; OA ↔ SA traceability displays counterparts.",
|
||||
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships explores one layer; OA ↔ SA traceability can show placed elements from both layers and their correspondence lines alongside ordinary relationships.",
|
||||
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
|
||||
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
|
||||
"Select a diagram node to access Placement, Fold / unfold and Hide. With the canvas focused, Delete hides its placement; Shift+Delete asks to remove the model element. Other views retain a hidden element. Undo restores either change.",
|
||||
@@ -317,6 +317,16 @@ fn article(topic: Topic) -> Article {
|
||||
"Keep diagrams readable",
|
||||
"Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.",
|
||||
),
|
||||
walkthrough(
|
||||
"Show a correspondence between OA and SA",
|
||||
&[
|
||||
"Choose OA ↔ SA traceability in View content. Changing this setting does not create counterparts or add their placements, so an existing single-layer diagram can initially look unchanged.",
|
||||
"Use Review layer traces to inspect named Operational and System endpoints and the meaning of their link. This opens the model's layer-trace list with no search restriction; traces between relationships are reviewed here rather than drawn between element cards.",
|
||||
"In the chosen architecture view, use Add existing elements. The chooser offers compatible elements from both layers. Select an existing endpoint and click its position in the canvas; repeat for the other end if needed. This adds a placement, not a new semantic element or trace. For port correspondences, place the ports' owners.",
|
||||
"The view shows correspondence lines alongside ordinary relationships. The shown-line count reflects the current projection and visibility settings, not trace coverage. If a line is hidden, use View visibility and inspect Saved view visibility; placing an endpoint does not remove explicit hides or filters.",
|
||||
"If the counterpart is already placed, use Include already shown (Locate) in the chooser. Undo restores an added placement; saving preserves each view's placements independently. Use the Traceability page's existing counterpart or manual-trace workflow when the semantic link itself does not yet exist.",
|
||||
],
|
||||
),
|
||||
walkthrough(
|
||||
"Move work between performers",
|
||||
&[
|
||||
|
||||
@@ -458,7 +458,8 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
entries.push(item(
|
||||
"Show in this diagram",
|
||||
Message::PlaceSelected,
|
||||
d.layer == e.layer() && d.projection.includes(e.kind()),
|
||||
(d.layer == e.layer() || d.projection == ProjectionKind::Traceability)
|
||||
&& d.projection.includes(e.kind()),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -589,6 +590,7 @@ fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
include!("model_menu_traceability_tests.rs");
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#[test]
|
||||
fn traceability_show_menu_matches_cross_layer_and_port_placement_eligibility() {
|
||||
let mut project = Project::empty();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Counterpart", None)
|
||||
.unwrap();
|
||||
let port = Id::new_v4();
|
||||
project.elements.insert(
|
||||
port,
|
||||
Element {
|
||||
id: port,
|
||||
alias: "legacy_port".into(),
|
||||
name: "Port".into(),
|
||||
description: String::new(),
|
||||
parent: Some(sa),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
|
||||
let mut p = project.clone();
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
for object in [sa, port] {
|
||||
send(&mut app, open(object, Point::ORIGIN));
|
||||
let action = popup(&app)
|
||||
.unwrap()
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|e| match e {
|
||||
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
|
||||
Some(message)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if object == port {
|
||||
assert!(
|
||||
action.is_none(),
|
||||
"Ports never acquire independent node placements"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
action.expect("non-port placement entry").is_some(),
|
||||
projection == ProjectionKind::Traceability
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,6 +599,28 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
if let Some(diagram) = app.active_diagram {
|
||||
let view = oa_core::projection::project_view(app.project(), diagram);
|
||||
if projection == Some(ProjectionKind::Traceability) {
|
||||
let correspondence_lines = view.edges.iter().filter(|edge| edge.trace).count();
|
||||
tools = tools.push(
|
||||
column![
|
||||
label("Shows OA and SA elements together, with correspondence lines alongside ordinary relationships."),
|
||||
label("Place both ends of a trace to show its line. Port traces use their owner cards; traces between relationships are reviewed on the Traceability page."),
|
||||
row![
|
||||
label(format!("{correspondence_lines} correspondence lines in this view")),
|
||||
action(
|
||||
"Add existing elements…",
|
||||
Message::Architecture(crate::app::architecture_authoring::Event::Existing(None))
|
||||
),
|
||||
action("Review layer traces", Message::ReviewLayerTraces),
|
||||
action("View visibility…", Message::Select(diagram)),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
.wrap(),
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
}
|
||||
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
|
||||
let promoted = edge.is_some_and(|edge| {
|
||||
oa_core::projection::edge_endpoint_placements(
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Traceability view comprehension — issue 31
|
||||
|
||||
Branch-specific work above `b8685416f56e53e30bd11ecf93a54cea10ac8458`.
|
||||
[Issue31](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/31)
|
||||
owns live progress. This note records the chosen design and acceptance plan,
|
||||
not completed validation or integration.
|
||||
|
||||
The user's observed problem is that OA↔SA traceability looks like All
|
||||
relationships and its purpose is unclear. Existing core behavior already
|
||||
projects both layers' placed elements, ordinary relationships and explicit
|
||||
traces with available endpoints. Switching projection neither creates semantic
|
||||
counterparts nor adds missing placements. Existing Add existing supports both
|
||||
layers, but its default suggested-type filter conceals opposite-layer choices.
|
||||
Show in this diagram also has inconsistent cross-layer eligibility and selects
|
||||
an opposite-layer element before placement, potentially changing the target view.
|
||||
|
||||
## Decision and criteria
|
||||
|
||||
Make the existing workflow understandable and reachable before introducing a
|
||||
second counterpart browser. Criteria: a new user can explain the distinction,
|
||||
find both named ends, place existing counterparts in the chosen saved diagram,
|
||||
see/inspect correspondence, understand absent lines, and preserve semantic data.
|
||||
Keep the established mixed ordinary/trace contract, explicit semantic creation,
|
||||
saved view filters and single-operation placement history.
|
||||
|
||||
Use concise projection-local explanation, the actual count of projected trace
|
||||
edges, Add existing elements, Review layer traces, and existing View visibility.
|
||||
Default the Existing chooser to All compatible types only for Traceability;
|
||||
creation and ordinary-view defaults stay scoped to their existing contexts.
|
||||
Align Show in this diagram with actual supported placement and retain the
|
||||
captured diagram across cross-layer selection. Review layer traces opens a
|
||||
predictable layer-trace inventory without a stale artifact filter/search, through
|
||||
existing navigation/draft guards. It does not promise that subsequently choosing
|
||||
an opposite-layer endpoint preserves the originating diagram.
|
||||
|
||||
The count is exactly `project_view(...).edges.filter(trace).count()`, after saved
|
||||
filters. It is neither a project-wide trace total nor a coverage denominator.
|
||||
No overlapping missing/hidden reasons are added into invented totals. Port
|
||||
correspondences use owner-card placements; relation-to-relation correspondences
|
||||
remain reviewable semantic records and are not promised a diagram line.
|
||||
|
||||
An expandable view-local counterpart browser with named Place/Locate actions
|
||||
was independently proposed. It can reduce name memorization, but introduces
|
||||
another scope, owner-mapping, omission-reason and targeted-placement interface.
|
||||
The existing chooser already completes the bounded placement task once its
|
||||
default and shortcut are corrected. Revisit the richer browser if the actual
|
||||
native task remains confusing. Tooltip-only, traces-only replacement, automatic
|
||||
counterpart creation/placement and forced OA/SA layout were rejected: respectively
|
||||
weak task completion, broken mixed-context contract, surprising semantics/history,
|
||||
and unwanted authored-layout changes.
|
||||
|
||||
## Verification plan
|
||||
|
||||
Exercise the real Existing chooser and actual Show menu path for opposite-layer
|
||||
elements, including stale/pending refusal and ordinary-view restrictions. Only
|
||||
the current diagram's intended placement may change; preserve semantic maps,
|
||||
other views, route/visibility choices, exact source Undo/Redo and save/reopen.
|
||||
Ports cannot become node placements. An already placed element must not acquire
|
||||
a duplicate placement. Predictable trace review must preserve drafts and obey
|
||||
the usual modal/navigation guards.
|
||||
|
||||
Use core projection/filter evidence for no trace, missing endpoints, both placed,
|
||||
ordinary OA/SA edges, multiple traces, explicit hides plus disabled Trace kind,
|
||||
port owners and relation correspondence. Do not infer visible lines from
|
||||
unfiltered edge_availability or confuse unrelated model traces with counterparts.
|
||||
|
||||
Native acceptance starts with an OA-owned mixed view, one already visible
|
||||
correspondence pair and another existing named SA counterpart not yet placed.
|
||||
Find/place it through the visible controls, see/select the additional trace,
|
||||
inspect an absent-line visibility reason, Undo/Redo and Save/reopen. Inspect
|
||||
toolbar/canvas space and chooser clarity at a realistic window size. Different
|
||||
help text or count tests alone do not resolve the user's comprehension complaint.
|
||||
Linux remains user-paused; Codex-956atf retains shared integration ownership.
|
||||
Reference in New Issue
Block a user