Fix scoped semantic drafts and dependent review controls
This commit is contained in:
+150
-31
@@ -45,6 +45,7 @@ pub struct Draft {
|
||||
baseline: Value,
|
||||
pub inputs: BTreeMap<String, Input>,
|
||||
pub exact_strings: bool,
|
||||
context: Option<Value>,
|
||||
port_contract_drafts: BTreeMap<String, Value>,
|
||||
pub value_browsers: BTreeMap<String, ValueBrowser>,
|
||||
}
|
||||
@@ -83,6 +84,7 @@ impl Draft {
|
||||
value,
|
||||
inputs: BTreeMap::new(),
|
||||
exact_strings: false,
|
||||
context: None,
|
||||
port_contract_drafts: BTreeMap::new(),
|
||||
value_browsers: BTreeMap::new(),
|
||||
};
|
||||
@@ -142,6 +144,16 @@ impl Draft {
|
||||
pub fn materialize(&self) -> Result<Value, String> {
|
||||
self.materialize_inputs(|_| true)
|
||||
}
|
||||
/// Read-only context for dependent controls. Workflow proposals never replace
|
||||
/// the authored value or its input buffers and are not used by command().
|
||||
pub fn context_value(&self) -> Value {
|
||||
self.context
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.materialize().unwrap_or_else(|_| self.value.clone()))
|
||||
}
|
||||
pub fn set_context(&mut self, context: Option<Value>) {
|
||||
self.context = context;
|
||||
}
|
||||
/// Materialize selected scalar subtrees without parsing retained, unselected drafts.
|
||||
pub fn materialize_scopes(&self, scopes: &[String]) -> Result<Value, String> {
|
||||
self.materialize_inputs(|path| {
|
||||
@@ -226,6 +238,97 @@ impl Draft {
|
||||
self.rebuild();
|
||||
Ok(())
|
||||
}
|
||||
/// Replace a workflow-authorized subtree without parsing unrelated inputs,
|
||||
/// including an invalid value which this replacement deliberately removes.
|
||||
pub fn shape_scoped(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
*self
|
||||
.value
|
||||
.pointer_mut(path)
|
||||
.ok_or("Property no longer exists.")? = value;
|
||||
self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) {
|
||||
let previous = std::mem::take(&mut self.inputs);
|
||||
self.rebuild();
|
||||
for (path, input) in previous {
|
||||
if let Some(path) = new_path(&path)
|
||||
&& self.inputs.contains_key(&path)
|
||||
{
|
||||
self.inputs.insert(path, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn array_scoped(
|
||||
&mut self,
|
||||
path: &str,
|
||||
index: Option<usize>,
|
||||
direction: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut candidate = self.value.clone();
|
||||
let count = candidate
|
||||
.pointer(path)
|
||||
.and_then(Value::as_array)
|
||||
.ok_or("List no longer exists.")?
|
||||
.len();
|
||||
edit_array(&mut candidate, path, index, direction)?;
|
||||
self.value = candidate;
|
||||
self.rebuild_retaining(|old| {
|
||||
let Some(tail) = old.strip_prefix(&format!("{path}/")) else {
|
||||
return Some(old.to_owned());
|
||||
};
|
||||
let (number, rest) = tail.split_once('/').unwrap_or((tail, ""));
|
||||
let position = number.parse::<usize>().ok()?;
|
||||
let position = match index {
|
||||
Some(index) if direction == 0 && position == index => return None,
|
||||
Some(index) if direction == 0 && position > index => position - 1,
|
||||
Some(index) if direction != 0 => {
|
||||
let target = (index as i32 + direction).clamp(0, count as i32 - 1) as usize;
|
||||
if position == index {
|
||||
target
|
||||
} else if position == target {
|
||||
index
|
||||
} else {
|
||||
position
|
||||
}
|
||||
}
|
||||
_ => position,
|
||||
};
|
||||
Some(if rest.is_empty() {
|
||||
format!("{path}/{position}")
|
||||
} else {
|
||||
format!("{path}/{position}/{rest}")
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
pub fn append_scoped(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
self.value
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("List no longer exists.")?
|
||||
.push(value);
|
||||
self.rebuild_retaining(|path| Some(path.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
pub fn value_path_scoped(
|
||||
&mut self,
|
||||
path: &str,
|
||||
key: crate::data_values::FieldPathKey,
|
||||
) -> Result<(), String> {
|
||||
let row = self
|
||||
.value
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or("Message value no longer exists.")?;
|
||||
row.insert("path".into(), json!(key.path));
|
||||
row.insert("field".into(), json!(key.field));
|
||||
self.rebuild_retaining(|old| {
|
||||
(!within(old, &format!("{path}/path")) && !within(old, &format!("{path}/field")))
|
||||
.then(|| old.to_owned())
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
pub fn array(
|
||||
&mut self,
|
||||
path: &str,
|
||||
@@ -233,36 +336,7 @@ impl Draft {
|
||||
direction: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut candidate = self.materialize()?;
|
||||
let items = candidate
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("List no longer exists.")?;
|
||||
match index {
|
||||
None => items.push(if path.ends_with("literals") {
|
||||
json!("New value")
|
||||
} else if path.ends_with("bindings") {
|
||||
json!({"referenced": Id::nil(), "local": Id::nil()})
|
||||
} else if path.ends_with("values") {
|
||||
json!({"path": [], "field": Id::nil(), "expression": ""})
|
||||
} else if path.ends_with("routes") {
|
||||
json!({"edge": Id::nil(), "waypoints": [], "label": null})
|
||||
} else if path.ends_with("waypoints") {
|
||||
json!({"x": 0.0, "y": 0.0})
|
||||
} else if path.ends_with("port_anchors") {
|
||||
json!({"port": Id::nil(), "side": "Right", "offset": 0.5})
|
||||
} else {
|
||||
json!(Id::nil())
|
||||
}),
|
||||
Some(i) if i < items.len() => {
|
||||
if direction == 0 {
|
||||
items.remove(i);
|
||||
} else {
|
||||
let target = (i as i32 + direction).clamp(0, items.len() as i32 - 1) as usize;
|
||||
items.swap(i, target);
|
||||
}
|
||||
}
|
||||
_ => return Err("List item no longer exists.".into()),
|
||||
}
|
||||
edit_array(&mut candidate, path, index, direction)?;
|
||||
self.value = candidate;
|
||||
self.rebuild();
|
||||
Ok(())
|
||||
@@ -364,6 +438,51 @@ impl Draft {
|
||||
}
|
||||
}
|
||||
|
||||
fn within(path: &str, root: &str) -> bool {
|
||||
path == root
|
||||
|| path
|
||||
.strip_prefix(root)
|
||||
.is_some_and(|tail| tail.starts_with('/'))
|
||||
}
|
||||
fn edit_array(
|
||||
value: &mut Value,
|
||||
path: &str,
|
||||
index: Option<usize>,
|
||||
direction: i32,
|
||||
) -> Result<(), String> {
|
||||
let items = value
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("List no longer exists.")?;
|
||||
match index {
|
||||
None => items.push(if path.ends_with("literals") {
|
||||
json!("New value")
|
||||
} else if path.ends_with("bindings") {
|
||||
json!({"referenced": Id::nil(), "local": Id::nil()})
|
||||
} else if path.ends_with("values") {
|
||||
json!({"path": [], "field": Id::nil(), "expression": ""})
|
||||
} else if path.ends_with("routes") {
|
||||
json!({"edge": Id::nil(), "waypoints": [], "label": null})
|
||||
} else if path.ends_with("waypoints") {
|
||||
json!({"x": 0.0, "y": 0.0})
|
||||
} else if path.ends_with("port_anchors") {
|
||||
json!({"port": Id::nil(), "side": "Right", "offset": 0.5})
|
||||
} else {
|
||||
json!(Id::nil())
|
||||
}),
|
||||
Some(i) if i < items.len() => {
|
||||
if direction == 0 {
|
||||
items.remove(i);
|
||||
} else {
|
||||
let target = (i as i32 + direction).clamp(0, items.len() as i32 - 1) as usize;
|
||||
items.swap(i, target);
|
||||
}
|
||||
}
|
||||
_ => return Err("List item no longer exists.".into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn friendly(name: &str) -> String {
|
||||
match name {
|
||||
"id" => "UUID identity".into(),
|
||||
@@ -578,7 +697,7 @@ fn reference_choices(draft: &Draft, project: &Project, path: &str, current: Id)
|
||||
}
|
||||
|
||||
fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
let value = draft.materialize().unwrap_or_else(|_| draft.value.clone());
|
||||
let value = draft.context_value();
|
||||
if path.starts_with("/port_anchors/") && path.ends_with("/port") {
|
||||
let owner = value
|
||||
.get("element")
|
||||
|
||||
Reference in New Issue
Block a user