Fix scoped semantic drafts and dependent review controls
This commit is contained in:
@@ -3,7 +3,7 @@ use super::*;
|
||||
use iced::widget::column;
|
||||
|
||||
fn element(draft: &Draft, project: &Project) -> Option<Element> {
|
||||
serde_json::from_value(draft.materialize().unwrap_or_else(|_| draft.value.clone()))
|
||||
serde_json::from_value(draft.context_value())
|
||||
.ok()
|
||||
.or_else(|| project.elements.get(&draft.original).cloned())
|
||||
}
|
||||
@@ -113,9 +113,9 @@ pub(super) fn property<'a>(
|
||||
let minimum_path = path.replace("maximum", "minimum");
|
||||
json!(
|
||||
draft
|
||||
.materialize()
|
||||
.ok()
|
||||
.and_then(|v| v.pointer(&minimum_path).and_then(Value::as_u64))
|
||||
.context_value()
|
||||
.pointer(&minimum_path)
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1)
|
||||
.max(1)
|
||||
)
|
||||
@@ -311,7 +311,7 @@ fn message_value<'a>(
|
||||
value: &'a Value,
|
||||
path: &str,
|
||||
) -> View<'a, Message> {
|
||||
let edited = draft.materialize().unwrap_or_else(|_| draft.value.clone());
|
||||
let edited = draft.context_value();
|
||||
let row = edited.pointer(path).unwrap_or(value).clone();
|
||||
let current = serde_json::from_value::<MessageValue>(row.clone()).ok();
|
||||
let owner = element(draft, project);
|
||||
@@ -513,6 +513,257 @@ fn field_page(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn semantic_state(app: &crate::app::App) -> &crate::semantic_review::State {
|
||||
let Some(crate::workflow::Workflow::Semantic(state)) = &app.workflow else {
|
||||
panic!()
|
||||
};
|
||||
state
|
||||
}
|
||||
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::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;
|
||||
semantic_send(app, Event::Select(FacetSelector::Field(facet)));
|
||||
semantic_send(
|
||||
app,
|
||||
Event::Decide(FacetSelector::Field(facet), Some(decision)),
|
||||
);
|
||||
}
|
||||
fn default_buttons(draft: &Draft, project: &Project, path: &str) -> usize {
|
||||
use iced::advanced::widget::{Tree, tree::Tag};
|
||||
let button: View<'_, Message> = button(text("Button")).into();
|
||||
let tag = Tree::new(button.as_widget()).tag;
|
||||
let view = property(draft, project, draft.value.pointer(path).unwrap(), path).unwrap();
|
||||
fn count(tree: &Tree, tag: Tag) -> usize {
|
||||
usize::from(tree.tag == tag)
|
||||
+ tree
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| count(child, tag))
|
||||
.sum::<usize>()
|
||||
}
|
||||
count(&Tree::new(view.as_widget()), tag)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep() {
|
||||
use crate::semantic_review::{Event, State};
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Oa, "Source", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Sa, "Target", None)
|
||||
.unwrap();
|
||||
let operation = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Sa, "Operation base", None)
|
||||
.unwrap();
|
||||
let flow = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Sa, "Flow base", None)
|
||||
.unwrap();
|
||||
for (id, next) in [
|
||||
(oa, ExchangeMechanism::Operation),
|
||||
(sa, ExchangeMechanism::Flow),
|
||||
(operation, ExchangeMechanism::Operation),
|
||||
(flow, ExchangeMechanism::Flow),
|
||||
] {
|
||||
let Some(SharedBody::ExchangeItem { mechanism, .. }) =
|
||||
p.elements.get_mut(&id).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*mechanism = next;
|
||||
}
|
||||
let trace = Id::new_v4();
|
||||
p.traces.insert(
|
||||
trace,
|
||||
Trace {
|
||||
id: trace,
|
||||
alias: "item_trace".into(),
|
||||
name: "Review item".into(),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Manual,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
let mut state = State::new(&p, oa).unwrap();
|
||||
let facet = FacetSelector::ExchangeItem(ExchangeItemFacet::Mechanism);
|
||||
let path = "/body/Sa/Shared/ExchangeItem/supertype";
|
||||
let raw = state.draft.as_ref().unwrap().value.clone();
|
||||
state.update(
|
||||
&p,
|
||||
Event::Decide(facet.clone(), Some(SemanticDecision::TakeSource)),
|
||||
);
|
||||
assert!(state.can_apply(), "{:?}", state.error);
|
||||
let choices = reference_ids(state.draft.as_ref().unwrap(), &p, path);
|
||||
assert!(choices.contains(&operation));
|
||||
assert!(!choices.contains(&flow));
|
||||
assert_eq!(state.draft.as_ref().unwrap().value, raw);
|
||||
state.update(&p, Event::Decide(facet, Some(SemanticDecision::KeepTarget)));
|
||||
let choices = reference_ids(state.draft.as_ref().unwrap(), &p, path);
|
||||
assert!(!choices.contains(&operation));
|
||||
assert!(choices.contains(&flow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_review_combined_take_type_informs_actual_default_form_in_both_directions() {
|
||||
use crate::semantic_review::Event;
|
||||
for reverse in [false, true] {
|
||||
for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] {
|
||||
let mut p = Project::empty();
|
||||
let mut pair = vec![];
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let source = (layer == Layer::Oa) != reverse;
|
||||
let record = p
|
||||
.add_element(Kind::DataType, layer, "Record", None)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() =
|
||||
SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { supertype: None },
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
};
|
||||
let field = p
|
||||
.add_element(Kind::DataField, layer, "Payload", Some(record))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&field).unwrap().shared_mut().unwrap() =
|
||||
SharedBody::DataField {
|
||||
type_ref: if source {
|
||||
TypeRef::Boolean
|
||||
} else {
|
||||
TypeRef::Integer
|
||||
},
|
||||
minimum: 0,
|
||||
maximum: Some(1),
|
||||
unit: String::new(),
|
||||
direction: ParameterDirection::Unspecified,
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
pair.push(field);
|
||||
}
|
||||
let trace = Id::new_v4();
|
||||
p.traces.insert(
|
||||
trace,
|
||||
Trace {
|
||||
id: trace,
|
||||
alias: "payload_trace".into(),
|
||||
name: "Payload review".into(),
|
||||
description: String::new(),
|
||||
oa: pair[0],
|
||||
sa: pair[1],
|
||||
kind: TraceKind::Manual,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
let (source, target, layer) = if reverse {
|
||||
(pair[1], pair[0], "Oa")
|
||||
} else {
|
||||
(pair[0], pair[1], "Sa")
|
||||
};
|
||||
let root = format!("/body/{layer}/Shared/DataField");
|
||||
let default = format!("{root}/default");
|
||||
let type_path = format!("{root}/type_ref");
|
||||
let minimum = format!("{root}/minimum");
|
||||
let mut app = crate::app::App::with_project(p);
|
||||
let before = app.document.snapshot();
|
||||
let _ = app.update(Message::StartSemanticReview(source));
|
||||
// An unfinished candidate must not make the context fall back to
|
||||
// the old target type, or contaminate a later Keep decision.
|
||||
semantic_decide(&mut app, FieldFacet::Minimum, SemanticDecision::Candidate);
|
||||
semantic_send(&mut app, Event::Input(minimum.clone(), "-".into()));
|
||||
semantic_decide(&mut app, FieldFacet::Type, decision);
|
||||
if decision == SemanticDecision::Candidate {
|
||||
semantic_send(
|
||||
&mut app,
|
||||
Event::Variant(type_path.clone(), json!("Boolean")),
|
||||
);
|
||||
}
|
||||
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
|
||||
assert!(!semantic_state(&app).can_apply());
|
||||
let draft = semantic_state(&app).draft.as_ref().unwrap();
|
||||
assert!(matches!(
|
||||
element(draft, app.project()).unwrap().shared(),
|
||||
Some(SharedBody::DataField {
|
||||
type_ref: TypeRef::Boolean,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(draft.inputs[&minimum].raw, "-");
|
||||
semantic_decide(&mut app, FieldFacet::Minimum, SemanticDecision::KeepTarget);
|
||||
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
|
||||
assert!(semantic_state(&app).can_apply());
|
||||
let draft = semantic_state(&app).draft.as_ref().unwrap();
|
||||
assert_eq!(default_buttons(draft, app.project(), &default), 2);
|
||||
// These are the same element/definition/literal helpers used by
|
||||
// the visible Set literal default and Add literal buttons.
|
||||
let owner = element(draft, app.project()).unwrap();
|
||||
let Some(SharedBody::DataField { type_ref, .. }) = owner.shared() else {
|
||||
panic!()
|
||||
};
|
||||
let initial = literal(&definition(app.project(), type_ref).unwrap()).unwrap();
|
||||
assert_eq!(initial, json!({"Boolean": false}));
|
||||
if decision == SemanticDecision::TakeSource {
|
||||
assert_eq!(draft.value.pointer(&type_path), Some(&json!("Integer")));
|
||||
}
|
||||
// Reverting the type changes the controls without changing its
|
||||
// retained candidate. Storage constraints likewise read context.
|
||||
semantic_decide(&mut app, FieldFacet::Type, SemanticDecision::KeepTarget);
|
||||
assert!(matches!(
|
||||
element(semantic_state(&app).draft.as_ref().unwrap(), app.project())
|
||||
.unwrap()
|
||||
.shared(),
|
||||
Some(SharedBody::DataField {
|
||||
type_ref: TypeRef::Integer,
|
||||
..
|
||||
})
|
||||
));
|
||||
semantic_decide(&mut app, FieldFacet::Type, decision);
|
||||
semantic_decide(&mut app, FieldFacet::Storage, SemanticDecision::Candidate);
|
||||
semantic_send(
|
||||
&mut app,
|
||||
Event::Variant(format!("{root}/storage"), json!("Reference")),
|
||||
);
|
||||
assert_eq!(
|
||||
default_buttons(
|
||||
semantic_state(&app).draft.as_ref().unwrap(),
|
||||
app.project(),
|
||||
&default
|
||||
),
|
||||
0
|
||||
);
|
||||
semantic_decide(&mut app, FieldFacet::Storage, SemanticDecision::KeepTarget);
|
||||
assert_eq!(
|
||||
default_buttons(
|
||||
semantic_state(&app).draft.as_ref().unwrap(),
|
||||
app.project(),
|
||||
&default
|
||||
),
|
||||
2
|
||||
);
|
||||
semantic_decide(&mut app, FieldFacet::Default, SemanticDecision::Candidate);
|
||||
semantic_send(&mut app, Event::Shape(default, json!([initial])));
|
||||
assert!(
|
||||
semantic_state(&app).can_apply(),
|
||||
"{:?}",
|
||||
semantic_state(&app).error
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
|
||||
assert!(
|
||||
matches!(app.project().elements[&target].shared(), Some(SharedBody::DataField {
|
||||
type_ref: TypeRef::Boolean, default: Some(values), minimum: 0, storage: FieldStorage::Value, ..
|
||||
}) if values == &vec![DataValue::Boolean(false)])
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_qualified_path_preserves_expression_typed_before_the_next_render() {
|
||||
let (mut f, next) = crate::data_values::tests::recursive_payload();
|
||||
|
||||
Reference in New Issue
Block a user