mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
variables: highlighted row owns Delete, Options and Select Bound Object
Select Bound Object ran variable.unbind on the selection, so choosing it silently removed the bindings; Delete Variable and Delete Data Set deleted every row. Now the panel keeps a highlighted variable row (a click highlights and selects what it drives, as the reference panel does): Select Bound Object selects what the highlighted variable drives via select.set, Delete Variable deletes only it, and Delete Data Set deletes only the current set. Variable Options and Rename Data Set open their dialogs preselected with the highlighted variable / active set (the double-click used to invoke variable.rename with a bare name, which only errored). Unbind enables only over a bound selection; Make Text Dynamic only for exactly one type object. The highlight is per-document session state with its own command (variable.highlight, no undo step, like layer.highlight): rename carries it, delete clears it when stale. New engine tests (18) and panel tests cover the reviewer's regression case, verified to fail on the old wiring.
This commit is contained in:
@@ -104,6 +104,12 @@ fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// The highlight names a variable, so it follows the rename rather than going stale.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_deref() == Some(from.as_str()) {
|
||||
st.variables_highlight = Some(to.clone());
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"from": from, "name": to}))
|
||||
}
|
||||
|
||||
@@ -149,6 +155,12 @@ fn make_dynamic(s: &mut Session, kind: VariableKind, stem: &str) -> Result<Value
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let names = names_param(p, "variable.delete")?;
|
||||
let n = s.edit("Delete Variables", |d, _| Ok(d.variables.prune(&names)))?;
|
||||
// A highlight of a variable that is gone highlights nothing.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_ref().is_some_and(|h| st.doc.variables.variable(h).is_none()) {
|
||||
st.variables_highlight = None;
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"deleted": n}))
|
||||
}
|
||||
|
||||
@@ -227,6 +239,27 @@ fn unbind(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"unbound": n}))
|
||||
}
|
||||
|
||||
/// Highlight one Variables panel row: what the panel's Delete, Options… and Select Bound
|
||||
/// Object act on. Panel state, not art selection: not saved, not an undo step (an empty
|
||||
/// or missing `variable` clears it), like `layer.highlight`.
|
||||
fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "variable.highlight";
|
||||
let name = match str_param(p, "variable").filter(|v| !v.is_empty()) {
|
||||
Some(v) => {
|
||||
let v = v.to_string();
|
||||
if s.doc()?.doc.variables.variable(&v).is_none() {
|
||||
return Err(bad(C, format!("no variable named `{v}`")));
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let st = s.doc_mut()?;
|
||||
st.variables_highlight = name.clone();
|
||||
st.revision += 1;
|
||||
Ok(json!({"variable": name}))
|
||||
}
|
||||
|
||||
fn dataset_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "dataset.new";
|
||||
let name = var_name(p, C)?;
|
||||
@@ -480,6 +513,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
unbind
|
||||
),
|
||||
cmd!(
|
||||
"variable.highlight",
|
||||
"Highlight Variable",
|
||||
[],
|
||||
None,
|
||||
"{variable?} highlight one Variables panel row (a click); what the panel's Delete, Options… and Select Bound Object act on. An empty or missing `variable` clears it. Not an undo step → {variable}",
|
||||
has_doc,
|
||||
highlight
|
||||
),
|
||||
cmd!("dataset.new", "New Data Set…", ["Window", "Variables"], None, "{name, values?: {variable: value}} add a dataset", has_doc, dataset_new),
|
||||
cmd!(
|
||||
"dataset.set",
|
||||
|
||||
@@ -86,6 +86,7 @@ pub struct Interaction {
|
||||
/// isolation).
|
||||
pub active_layer: Option<NodeId>,
|
||||
pub layer_rows: Vec<NodeId>,
|
||||
pub variables_highlight: Option<String>,
|
||||
pub isolation: Option<NodeId>,
|
||||
/// The perspective transform the previews make (`perspective.transform` params): Transform
|
||||
/// Again repeats it once the drag is committed.
|
||||
@@ -121,6 +122,10 @@ pub struct DocState {
|
||||
/// Panel state, not art selection: not saved, not undoable (`layer.setCurrent`,
|
||||
/// `layer.highlight`).
|
||||
pub layer_rows: Vec<NodeId>,
|
||||
/// The variable highlighted in the Variables panel: what its Delete, Options… and Select
|
||||
/// Bound Object act on. Panel state, not art selection: not saved, not undoable
|
||||
/// (`variable.highlight`). A name, so renaming a variable moves the highlight with it.
|
||||
pub variables_highlight: Option<String>,
|
||||
/// Isolation mode container.
|
||||
pub isolation: Option<NodeId>,
|
||||
pub interaction: Option<Interaction>,
|
||||
@@ -236,6 +241,7 @@ impl DocState {
|
||||
revision: 1,
|
||||
active_layer,
|
||||
layer_rows: vec![],
|
||||
variables_highlight: None,
|
||||
isolation: None,
|
||||
interaction: None,
|
||||
undo_group: None,
|
||||
@@ -300,6 +306,7 @@ impl DocState {
|
||||
self.selection = it.selection;
|
||||
self.active_layer = it.active_layer;
|
||||
self.layer_rows = it.layer_rows;
|
||||
self.variables_highlight = it.variables_highlight;
|
||||
self.isolation = it.isolation;
|
||||
self.revision += 1;
|
||||
}
|
||||
@@ -1294,6 +1301,7 @@ impl Session {
|
||||
preview: None,
|
||||
active_layer: st.active_layer,
|
||||
layer_rows: st.layer_rows.clone(),
|
||||
variables_highlight: st.variables_highlight.clone(),
|
||||
isolation: st.isolation,
|
||||
perspective_again: None,
|
||||
});
|
||||
|
||||
@@ -436,3 +436,56 @@ fn bad_variables_requests_are_errors_not_panics() {
|
||||
assert!(s.execute(cmd, ¶ms).is_err(), "{cmd} {params}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_selects_without_unbinding_and_deletes_leave_the_rest() {
|
||||
// The reviewer's regression case: two variables, two rows.
|
||||
let mut s = session();
|
||||
let a = text(&mut s, "Alice");
|
||||
let b = text(&mut s, "Bob");
|
||||
for (name, id) in [("Name", a), ("Other", b)] {
|
||||
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": name, "ids": [id.0]})).unwrap();
|
||||
}
|
||||
s.execute("dataset.new", &json!({"name": "One", "values": {"Name": "Alice", "Other": "Bob"}})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Two", "values": {"Name": "A2", "Other": "B2"}})).unwrap();
|
||||
s.execute("dataset.select", &json!({"name": "One"})).unwrap();
|
||||
|
||||
// What a row click runs (highlight), then what Select Bound Object runs (select).
|
||||
s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
|
||||
let bound: Vec<u64> = s.doc().unwrap().doc.variables.objects_of("Name").iter().map(|i| i.0).collect();
|
||||
s.execute("select.set", &json!({"ids": bound})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
|
||||
// …and the binding is still there: selecting never unbinds.
|
||||
assert_eq!(s.doc().unwrap().doc.variables.objects_of("Name"), vec![a]);
|
||||
|
||||
// Deleting one variable leaves the other; deleting the current dataset leaves the other.
|
||||
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
|
||||
let vars = s.execute("variable.list", &json!({})).unwrap();
|
||||
assert_eq!(vars["variables"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(vars["variables"][0]["name"], "Other");
|
||||
s.execute("dataset.delete", &json!({"names": ["One"]})).unwrap();
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(sets["datasets"][0]["name"], "Two");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_follows_rename_and_clears_with_delete() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
|
||||
assert!(s.execute("variable.highlight", &json!({"variable": "Missing"})).is_err(), "unknown names stay errors");
|
||||
let v = s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
|
||||
assert_eq!(v["variable"], "Name");
|
||||
// The highlight names a variable, so the rename carries it along.
|
||||
s.execute("variable.rename", &json!({"name": "Name", "newName": "Title"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().variables_highlight.as_deref(), Some("Title"));
|
||||
// Empty clears it, and deleting the highlighted variable clears it too.
|
||||
s.execute("variable.highlight", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().variables_highlight.is_none());
|
||||
s.execute("variable.highlight", &json!({"variable": "Title"})).unwrap();
|
||||
s.execute("variable.delete", &json!({"names": ["Title"]})).unwrap();
|
||||
assert!(s.doc().unwrap().variables_highlight.is_none(), "a highlight of a deleted variable highlights nothing");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user