Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
+151
-54
@@ -1,3 +1,4 @@
|
||||
pub(crate) mod traceability;
|
||||
use crate::{
|
||||
app::{App, Choice, FileOperation, Message, Page, Pending},
|
||||
diagram::DiagramCanvas,
|
||||
@@ -192,9 +193,11 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.push(crate::explorer_view::sidebar(app))
|
||||
.push(creation)
|
||||
.push(action("Project properties", Message::SelectProject).width(Fill));
|
||||
.push(crate::explorer_view::sidebar(app));
|
||||
if app.page != Page::Diagram {
|
||||
nav = nav.push(creation);
|
||||
}
|
||||
nav = nav.push(action("Project properties", Message::SelectProject).width(Fill));
|
||||
let sidebar = container(if app.page == Page::Text {
|
||||
source_files(app)
|
||||
} else {
|
||||
@@ -224,12 +227,33 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
Page::Validation => checks(app),
|
||||
Page::Guide => crate::guide::view(&app.guide),
|
||||
};
|
||||
let process_layout = app.topology_context().is_some();
|
||||
let focus_process = process_layout && app.behavior.topology.focused;
|
||||
let mut body = if app.page == Page::Home {
|
||||
row![container(main).width(Fill).height(Fill)].height(Fill)
|
||||
} else if process_layout {
|
||||
// Keep main at the same widget-tree path across Focus toggles. The
|
||||
// collapsed slot has no controls, so it cannot paint over or hit the canvas.
|
||||
let process_sidebar: View<'_, Message> = if focus_process {
|
||||
Space::new().width(0).into()
|
||||
} else {
|
||||
sidebar.into()
|
||||
};
|
||||
// Row::push would discard the zero-width slot and shift main again.
|
||||
widget::Row::from_vec(vec![
|
||||
container(process_sidebar)
|
||||
.width(if focus_process { 0 } else { 260 })
|
||||
.height(Fill)
|
||||
.clip(true)
|
||||
.into(),
|
||||
container(main).width(Fill).height(Fill).into(),
|
||||
])
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
} else {
|
||||
row![sidebar, container(main).width(Fill).height(Fill)].height(Fill)
|
||||
};
|
||||
if !matches!(app.page, Page::Home | Page::Text | Page::Guide) {
|
||||
if !focus_process && !matches!(app.page, Page::Home | Page::Text | Page::Guide) {
|
||||
body = body.push(inspector(app));
|
||||
}
|
||||
let footer = container(
|
||||
@@ -259,6 +283,13 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
.padding([4, 16])
|
||||
.style(style::panel);
|
||||
let root: View<'_, Message> = column![header, task_nav, body, footer].into();
|
||||
if app.architecture.as_ref().is_some_and(|s| s.modal()) {
|
||||
return modal_width(
|
||||
root,
|
||||
crate::app::architecture_authoring::dialog(app),
|
||||
(app.window_size.width - 50.).clamp(500., 820.),
|
||||
);
|
||||
}
|
||||
if let Some(workflow) = &app.workflow {
|
||||
if matches!(workflow, crate::workflow::Workflow::Semantic(_)) {
|
||||
return modal_width(
|
||||
@@ -345,23 +376,17 @@ fn modal_width<'a>(
|
||||
.into()
|
||||
}
|
||||
fn diagram(app: &App) -> View<'_, Message> {
|
||||
let diagrams = app
|
||||
let mut diagrams = app
|
||||
.project()
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| d.layer == app.layer || d.projection == ProjectionKind::Traceability)
|
||||
.map(|d| Choice {
|
||||
id: d.id,
|
||||
label: short(
|
||||
&d.name,
|
||||
if app.window_size.width < 1250. {
|
||||
26
|
||||
} else {
|
||||
48
|
||||
},
|
||||
),
|
||||
label: d.name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
diagrams.sort_by(|a, b| a.label.cmp(&b.label).then(a.id.cmp(&b.id)));
|
||||
let selected = diagrams
|
||||
.iter()
|
||||
.find(|c| Some(c.id) == app.active_diagram)
|
||||
@@ -427,16 +452,26 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.filter(|(edge, _)| *edge == app.selected)
|
||||
.map(|(_, i)| i),
|
||||
editable: !app.draft_dirty() && !app.source_dirty(),
|
||||
placing: app.architecture.as_ref().is_some_and(|s| s.saved.placing),
|
||||
connection: app.connection.as_ref(),
|
||||
};
|
||||
widget::sensor(widget::stack![
|
||||
let mut scene = widget::stack![
|
||||
widget::canvas(program).width(Fill).height(Fill),
|
||||
widget::canvas(crate::diagram::PortLabels(program))
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
])
|
||||
.on_show(Message::CanvasResized)
|
||||
.on_resize(Message::CanvasResized)
|
||||
];
|
||||
if let Some(editor) = crate::app::architecture_authoring::inline_editor(app) {
|
||||
scene = scene.push(editor);
|
||||
}
|
||||
row![
|
||||
widget::sensor(scene)
|
||||
.on_show(Message::CanvasResized)
|
||||
.on_resize(Message::CanvasResized),
|
||||
crate::app::architecture_authoring::palette(app)
|
||||
]
|
||||
.height(Fill)
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
container(
|
||||
@@ -459,20 +494,10 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.center(Fill)
|
||||
.into()
|
||||
};
|
||||
let choose = |tool: Option<crate::connection_tool::Tool>| {
|
||||
let active = app.connection.as_ref().map(|s| s.tool) == tool;
|
||||
button(text(tool.map_or("Select / move", |t| t.label())).size(12))
|
||||
.padding([8, 10])
|
||||
.on_press(Message::Connection(crate::connection_edit::Event::Choose(
|
||||
tool,
|
||||
)))
|
||||
.style(if active { button::primary } else { style::nav })
|
||||
};
|
||||
let mut palette = row![choose(None)].spacing(4);
|
||||
for tool in crate::connection_tool::Tool::for_layer(app.layer) {
|
||||
palette = palette.push(choose(Some(*tool)));
|
||||
let mut tools = column![toolbar].spacing(8);
|
||||
if app.active_diagram.is_some() {
|
||||
tools = tools.push(modes);
|
||||
}
|
||||
let mut tools = column![toolbar, modes, palette].spacing(8);
|
||||
if let Some(diagram) = app.active_diagram {
|
||||
let view = oa_core::projection::project_view(app.project(), diagram);
|
||||
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
|
||||
@@ -726,12 +751,22 @@ fn inspector(app: &App) -> View<'_, Message> {
|
||||
);
|
||||
}
|
||||
if let Some(r) = p.relations.get(&app.selected) {
|
||||
body = body.push(text(r.kind().to_string()).size(17)).push(
|
||||
pick_list(RelationKind::ALL, Some(r.kind()), Message::RelationKind)
|
||||
body = body.push(text(r.kind().to_string()).size(17));
|
||||
if r.kind() != RelationKind::ContextualLink {
|
||||
body = body.push(
|
||||
pick_list(
|
||||
RelationKind::ALL
|
||||
.into_iter()
|
||||
.filter(|kind| *kind != RelationKind::ContextualLink)
|
||||
.collect::<Vec<_>>(),
|
||||
Some(r.kind()),
|
||||
Message::RelationKind,
|
||||
)
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
body = body.push(
|
||||
row![
|
||||
@@ -771,27 +806,81 @@ fn data_catalog(app: &App) -> View<'_, Message> {
|
||||
|
||||
fn traces(app: &App) -> View<'_, Message> {
|
||||
let p = app.project();
|
||||
let mut out = column![
|
||||
use traceability::{Event as E, Filter};
|
||||
let (traces, artifacts) = app.traceability.matching(p);
|
||||
let mut tabs = row![].spacing(6);
|
||||
for (filter, label) in [
|
||||
(
|
||||
Filter::All,
|
||||
format!("All ({})", p.traces.len() + p.artifact_links.len()),
|
||||
),
|
||||
(Filter::Traces, format!("Layer traces ({})", p.traces.len())),
|
||||
(
|
||||
Filter::Artifacts,
|
||||
format!("Development artifacts ({})", p.artifact_links.len()),
|
||||
),
|
||||
] {
|
||||
tabs = tabs.push(
|
||||
button(text(label).size(12))
|
||||
.style(if app.traceability.filter == filter {
|
||||
button::primary
|
||||
} else {
|
||||
style::nav
|
||||
})
|
||||
.on_press(Message::Traceability(E::Filter(filter))),
|
||||
);
|
||||
}
|
||||
let header = column![
|
||||
heading(
|
||||
"Traceability",
|
||||
"Connect operational needs, system responsibilities and development evidence."
|
||||
),
|
||||
row![
|
||||
button(text("Create counterpart / review prose…").size(12))
|
||||
.padding(9)
|
||||
.on_press(Message::StartTransition),
|
||||
button(
|
||||
text(
|
||||
if p.layer_of(app.selected).unwrap_or(app.layer) == Layer::Oa {
|
||||
"Create System counterpart…"
|
||||
} else {
|
||||
"Create Operational counterpart…"
|
||||
}
|
||||
)
|
||||
.size(12)
|
||||
)
|
||||
.padding(9)
|
||||
.on_press(Message::StartTransition),
|
||||
action("+ Manual trace", Message::AddTrace),
|
||||
action(
|
||||
"Review semantics…",
|
||||
"Review counterpart changes…",
|
||||
Message::StartSemanticReview(app.selected)
|
||||
),
|
||||
action("+ Artifact", Message::AddArtifact)
|
||||
]
|
||||
.spacing(8)
|
||||
.wrap()
|
||||
.wrap(),
|
||||
tabs.wrap(),
|
||||
row![
|
||||
text_input(
|
||||
"Search names, aliases, owners or artifact URI…",
|
||||
&app.traceability.query
|
||||
)
|
||||
.on_input(|raw| Message::Traceability(E::Search(raw)))
|
||||
.padding(8)
|
||||
.size(12)
|
||||
.width(Fill),
|
||||
button("Clear search").on_press(Message::Traceability(E::Search(String::new())))
|
||||
]
|
||||
.spacing(6),
|
||||
label(format!(
|
||||
"{} matching traces · {} matching artifacts",
|
||||
traces.len(),
|
||||
artifacts.len()
|
||||
))
|
||||
]
|
||||
.spacing(16);
|
||||
for t in p.traces.values() {
|
||||
.spacing(10)
|
||||
.padding(24);
|
||||
let mut out = column![].spacing(16);
|
||||
for id in &traces {
|
||||
let t = &p.traces[id];
|
||||
let current = match (&t.baseline, p.properties(t.oa), p.properties(t.sa)) {
|
||||
(Some(b), Some(oa), Some(sa)) if b.oa == oa && b.sa == sa => "Reviewed",
|
||||
(Some(_), _, _) => "Review needed",
|
||||
@@ -810,7 +899,10 @@ fn traces(app: &App) -> View<'_, Message> {
|
||||
row![
|
||||
label(format!("{:?} · Prose: {current}", t.kind)),
|
||||
Space::new().width(Fill),
|
||||
action("Review semantics…", Message::StartSemanticReview(t.id)),
|
||||
action(
|
||||
"Review counterpart changes…",
|
||||
Message::StartSemanticReview(t.id)
|
||||
),
|
||||
action("Edit link", Message::Select(t.id))
|
||||
],
|
||||
text(crate::semantic_review::summary(p, t))
|
||||
@@ -824,17 +916,12 @@ fn traces(app: &App) -> View<'_, Message> {
|
||||
.style(style::card),
|
||||
);
|
||||
}
|
||||
out = out.push(text("Development artifacts").size(18));
|
||||
for a in p.artifact_links.values() {
|
||||
let owner = p
|
||||
.diagrams
|
||||
.values()
|
||||
.find_map(|d| {
|
||||
d.nodes
|
||||
.get(&a.owner)
|
||||
.map(|n| format!("Placement of {} · {}", p.named(n.element), d.name))
|
||||
})
|
||||
.unwrap_or_else(|| p.named(a.owner).to_owned());
|
||||
if !artifacts.is_empty() {
|
||||
out = out.push(text("Development artifacts").size(18));
|
||||
}
|
||||
for id in &artifacts {
|
||||
let a = &p.artifact_links[id];
|
||||
let owner = traceability::owner_label(p, a.owner);
|
||||
out = out.push(
|
||||
container(
|
||||
column![
|
||||
@@ -860,8 +947,18 @@ fn traces(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
if p.traces.is_empty() && p.artifact_links.is_empty() {
|
||||
out=out.push(text("Select a model element, then connect it to the other perspective or attach a code, CAD, schematic or document reference.").size(14).color(style::MUTED));
|
||||
} else if traces.is_empty() && artifacts.is_empty() {
|
||||
out = out
|
||||
.push(text("No matching records. Clear the search or choose another filter.").size(13));
|
||||
}
|
||||
scrollable(out.padding(24)).into()
|
||||
column![
|
||||
header,
|
||||
scrollable(out.padding(iced::Padding::from(24).right(42)))
|
||||
.id("traceability-list")
|
||||
.height(Fill)
|
||||
]
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn source_files(app: &App) -> View<'_, Message> {
|
||||
|
||||
Reference in New Issue
Block a user