Keep contextual properties and creation bound to their semantic targets

This commit is contained in:
2026-09-24 23:30:56 -07:00
parent 603a59c178
commit ac69f555af
3 changed files with 288 additions and 11 deletions
+27 -8
View File
@@ -2766,7 +2766,15 @@ impl App {
}
self.reconcile_multiselect();
self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i));
return self.update(*action);
let action = match *action {
Message::EditProperties(id)
if menu.request.canvas.is_some() && id == menu.request.object =>
{
Message::InspectorTab(crate::navigation::InspectorTab::Properties)
}
action => action,
};
return self.update(action);
}
Message::CreateContextElement(kind, child) => {
self.create_kind = kind;
@@ -3377,7 +3385,7 @@ impl App {
self.workflow = Some(crate::workflow::Workflow::relationship_kind(
self.project(),
self.selected,
self.layer,
self.project().layer_of(self.selected).unwrap_or(self.layer),
kind,
incoming,
));
@@ -4403,13 +4411,18 @@ impl App {
self.status = "Select an SA function or participant, then choose a typed port action in Properties.".into();
return;
}
let creation_layer = if child {
self.project().layer_of(self.selected).unwrap_or(self.layer)
} else {
self.layer
};
let allocated = child
&& self.create_kind.is_work()
&& self
.project()
.elements
.get(&self.selected)
.is_some_and(|e| e.layer() == self.layer && e.kind().is_performer());
.is_some_and(|e| e.layer() == creation_layer && e.kind().is_performer());
let parent = if child && !allocated {
Some(self.selected)
} else {
@@ -4417,7 +4430,7 @@ impl App {
};
let Some(e) = self.project().make_element(
self.create_kind,
self.layer,
creation_layer,
&format!("New {}", self.create_kind.label().to_lowercase()),
parent,
) else {
@@ -4446,16 +4459,16 @@ impl App {
.project()
.diagrams
.values()
.find(|d| d.layer == self.layer && d.projection == preferred)
.find(|d| d.layer == creation_layer && d.projection == preferred)
.map(|d| d.id);
if diagram_target.is_none() {
let view = Id::new_v4();
commands.push(Command::CreateDiagram(Diagram {
id: view,
alias: self.project().fresh_alias("engineering_view", "view", view),
name: format!("{} {preferred}", self.layer),
name: format!("{} {preferred}", creation_layer),
description: String::new(),
layer: self.layer,
layer: creation_layer,
projection: preferred,
layout: None,
routes: Vec::new(),
@@ -4588,7 +4601,13 @@ impl App {
));
}
if self.execute(Command::Batch(commands)) {
let changed_view = self.active_diagram != diagram_target;
self.active_diagram = diagram_target;
if changed_view
&& let Some(view) = diagram_target.and_then(|id| self.project().diagrams.get(&id))
{
self.layer = view.layer;
}
self.selected = id;
self.inspector_tab = crate::navigation::InspectorTab::Properties;
self.refresh_draft();
@@ -4602,7 +4621,7 @@ impl App {
self.workflow = Some(crate::workflow::Workflow::relationship(
self.project(),
self.selected,
self.layer,
self.project().layer_of(self.selected).unwrap_or(self.layer),
));
}
fn add_trace(&mut self) {
@@ -295,3 +295,251 @@ fn cross_layer_port_inspection_and_connection_start_retain_the_view() {
assert_eq!(app.connection.as_ref().unwrap().source, Some(port));
assert!(!app.document.can_undo());
}
fn authoring_fixture(performer: bool) -> (Fixture, Id) {
let mut f = fixture();
let mut project = f.app.project().clone();
let owner = project
.add_element(
if performer {
Kind::Entity
} else {
Kind::Activity
},
Layer::Oa,
"Operational owner",
None,
)
.unwrap();
let target = project
.add_element(Kind::Activity, Layer::Oa, "Operational target", None)
.unwrap();
let placement = project.place(f.diagram, owner, Position { x: 0., y: 400. });
project.place(f.diagram, target, Position { x: 500., y: 400. });
f.app = App::with_project(project);
f.app.page = Page::Diagram;
f.app.layer = Layer::Sa;
f.app.active_diagram = Some(f.diagram);
f.app.selected = f.diagram;
f.app.refresh_draft();
f.app.reconcile_multiselect();
f.process = owner;
f.placement = placement;
(f, target)
}
fn assert_source_pair(app: &App) {
let compiled = oa_core::source_project::compile_bundle(
app.document.bundle(),
oa_core::source_project::ProjectLimits::default(),
)
.unwrap();
assert_eq!(&compiled.decoded.project, app.project());
assert_eq!(&compiled.bundle, app.document.bundle());
}
#[test]
fn mixed_canvas_relationship_actions_keep_operational_source_and_commit_one_step() {
for context_action in [false, true] {
let (mut f, target) = authoring_fixture(false);
let before = f.app.document.snapshot();
let owner = f.process;
let placement = f.placement;
if context_action {
invoke(
&mut f,
owner,
Some(placement),
Message::AddRelationKind(RelationKind::Interaction, false),
);
} else {
let _ = f.app.update(Message::SelectNode(placement));
let _ = f.app.update(Message::AddRelation);
}
assert_surface(&f);
let Some(crate::workflow::Workflow::Relation(relation)) = &f.app.workflow else {
panic!("relationship form did not open: {}", f.app.status);
};
assert_eq!(
relation.source.as_ref().map(|source| source.id),
Some(owner)
);
assert_eq!(relation.kind, RelationKind::Interaction);
let saved = f
.app
.workflow
.as_ref()
.unwrap()
.recovery_snapshot()
.unwrap();
assert_eq!(saved["Relation"]["layer"], "Oa");
let _ = f
.app
.update(Message::Workflow(crate::workflow::Event::Target(Choice {
id: target,
label: "Operational target".into(),
})));
let _ = f
.app
.update(Message::Workflow(crate::workflow::Event::Apply));
assert!(f.app.workflow.is_none(), "{}", f.app.status);
let created: Vec<_> = f
.app
.project()
.relations
.values()
.filter(|relation| !before.decoded.project.relations.contains_key(&relation.id))
.collect();
assert_eq!(created.len(), 1);
assert_eq!(
(created[0].kind(), created[0].source(), created[0].target()),
(RelationKind::Interaction, owner, target)
);
assert_source_pair(&f.app);
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo());
}
}
#[test]
fn mixed_canvas_add_child_command_uses_parent_layer_for_containment_and_allocation() {
for performer in [false, true] {
let (mut f, _) = authoring_fixture(performer);
let before = f.app.document.snapshot();
let owner = f.process;
let placement = f.placement;
invoke(
&mut f,
owner,
Some(placement),
Message::CreateContextElement(Kind::Activity, true),
);
assert_surface(&f);
let created: Vec<_> = f
.app
.project()
.elements
.values()
.filter(|element| !before.decoded.project.elements.contains_key(&element.id))
.collect();
assert_eq!(created.len(), 1, "{}", f.app.status);
let child = created[0].id;
assert_eq!(created[0].kind(), Kind::Activity);
assert_eq!(created[0].layer(), Layer::Oa);
assert_eq!(
created[0].parent,
if performer { None } else { Some(owner) }
);
let allocations: Vec<_> = f
.app
.project()
.relations
.values()
.filter(|relation| !before.decoded.project.relations.contains_key(&relation.id))
.collect();
assert_eq!(allocations.len(), usize::from(performer));
if performer {
assert_eq!(
(
allocations[0].kind(),
allocations[0].source(),
allocations[0].target()
),
(RelationKind::Allocation, owner, child)
);
}
assert!(
f.app.project().diagrams[&f.diagram]
.nodes
.values()
.any(|node| node.element == child)
);
assert_source_pair(&f.app);
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo());
}
}
#[test]
fn mixed_canvas_root_creation_still_uses_the_active_perspective() {
let (mut f, _) = authoring_fixture(false);
let _ = f.app.update(Message::SelectNode(f.placement));
let before = f.app.document.snapshot();
let _ = f
.app
.update(Message::CreateContextElement(Kind::Function, false));
assert_surface(&f);
let created: Vec<_> = f
.app
.project()
.elements
.values()
.filter(|element| !before.decoded.project.elements.contains_key(&element.id))
.collect();
assert_eq!(created.len(), 1, "{}", f.app.status);
assert_eq!(created[0].kind(), Kind::Function);
assert_eq!(created[0].layer(), Layer::Sa);
assert_eq!(created[0].parent, None);
assert_source_pair(&f.app);
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
}
#[test]
fn child_creation_navigates_to_a_parent_layer_preferred_view_only_when_needed() {
for existing_preferred in [false, true] {
let (mut f, _) = authoring_fixture(false);
let mut project = f.app.project().clone();
for diagram in project
.diagrams
.values_mut()
.filter(|d| d.layer == Layer::Oa)
{
diagram.projection = if existing_preferred {
ProjectionKind::Allocation
} else {
ProjectionKind::Overview
};
}
f.app = App::with_project(project);
f.app.page = Page::Diagram;
f.app.layer = Layer::Sa;
f.app.active_diagram = Some(f.diagram);
f.app.selected = f.diagram;
f.app.refresh_draft();
let _ = f.app.update(Message::SelectNode(f.placement));
let _ = f
.app
.update(Message::Projection(ProjectionKind::Capabilities));
assert_eq!(f.app.selected, f.process);
let before = f.app.document.snapshot();
let _ = f
.app
.update(Message::CreateContextElement(Kind::Activity, true));
assert_eq!(f.app.layer, Layer::Oa, "{}", f.app.status);
let diagram = f.app.active_diagram.unwrap();
assert_ne!(diagram, f.diagram);
assert_eq!(f.app.project().diagrams[&diagram].layer, Layer::Oa);
assert_eq!(
f.app.project().diagrams[&diagram].projection,
ProjectionKind::Allocation
);
assert_eq!(
f.app.project().diagrams.len(),
before.decoded.project.diagrams.len() + usize::from(!existing_preferred)
);
assert_eq!(
f.app.project().elements[&f.app.selected].parent,
Some(f.process)
);
assert_source_pair(&f.app);
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
}
}
+13 -3
View File
@@ -29,8 +29,10 @@ Canvas inspection carries the originating route context and preserves view/layer
Explicit model navigation continues to follow the target layer. Draft validation
and stale-context rejection apply before inspection. Existing production group
selection already retained the canvas; fallback edge/port selection and canvas
context actions require the correction. Actual native reproduction must establish
which path accounts for the user's reported click; source diagnosis alone does not.
context actions require the correction. The published Windows baseline reproduces both reported failure states;
source diagnosis and those baseline results do not establish patched acceptance.
Contextual child/relationship creation must derive the semantic layer from the
selected object, while root creation continues to use the active perspective.
## Acceptance
@@ -49,4 +51,12 @@ which path accounts for the user's reported click; source diagnosis alone does n
Linux remains excluded by the existing user pause. Independent design critique
identified saved visibility selectors and frozen migration as additional admission
paths; both are included. Automated and native outcomes are not yet recorded.
paths; both are included. At published candidate `603a59c1785dbcca324dae063ecc3405d9519a30`, 41 targeted core checks pass: legacy admission (2), projection switching (3), review regressions (15), source-6 view contract (8), and state diagrams (13). Tracked inputs were unchanged across the run. Whole-workspace formatting also passed at that commit. The first `0341bca` core attempt failed a generated-UUID test oracle; explicit fixture identities corrected the evaluator, without relaxing the admission assertion.
The desktop attempt at `603a59c` was deliberately interrupted before test execution after independent source review found canvas Properties redispatch and contextual relationship/child semantic-layer defects. Its source inputs remained unchanged; no desktop passing result is claimed. The Properties issue arose from fixing explicit navigation to an already-inspected opposite-layer object. Keep both regressions: contextual Properties stays in the view, explicit navigation follows the object's layer.
Published-preview executable SHA256 `2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399` reproduces edge selection changing SA to OA, and Architecture switching refused with `diagram_layer`. The isolated valid fixture remained byte-for-byte unchanged after the trial. An earlier fixture used reserved alias `capability` and was rejected; it was corrected before either reproduction. Initial native-dialog capture/input failures also remain separate from application acceptance. Independent evidence review confirmed the resulting screenshot states and persisted byte equality; screenshots alone do not capture the complete action sequence or transient state history.
Candidate Windows GUI save/history/reopen, HTTP regression, broader affected suites and actual rendering acceptance remain outstanding. Raw local evidence is not yet a peer-consumable handoff; publish the consolidated evidence before integration.
Scope boundary: contextual relationship forms retain the canvas while opening; successful Apply retains the existing explicit post-review navigation. The generic child-creation handler is tested for containment and allocation, but the separate architecture palette/performer workflow still offers creation kinds for the diagram layer. Thus an OA performer displayed in an SA Traceability view has no Add activity menu entry; this inherited limitation is not claimed repaired here.