adding chart meta to supported backend data
This commit is contained in:
@@ -138,6 +138,16 @@ pub struct TaskTransition {
|
|||||||
/// Next tasks to invoke when transition criteria is met
|
/// Next tasks to invoke when transition criteria is met
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub r#do: Option<Vec<String>>,
|
pub r#do: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Frontend-only visual metadata (label, color, line style, waypoints).
|
||||||
|
/// Not consumed by the backend — preserved so the workflow builder can
|
||||||
|
/// restore its visual state after a round-trip through the parser.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
rename = "__chart_meta__",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub chart_meta: Option<JsonValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -226,6 +236,16 @@ pub struct Task {
|
|||||||
|
|
||||||
/// Parallel tasks (for parallel type)
|
/// Parallel tasks (for parallel type)
|
||||||
pub tasks: Option<Vec<Task>>,
|
pub tasks: Option<Vec<Task>>,
|
||||||
|
|
||||||
|
/// Frontend-only visual metadata (e.g. canvas position).
|
||||||
|
/// Not consumed by the backend — preserved so the workflow builder can
|
||||||
|
/// restore its visual state after a round-trip through the parser.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
rename = "__chart_meta__",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub chart_meta: Option<JsonValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
@@ -262,6 +282,7 @@ impl Task {
|
|||||||
when: Some("{{ succeeded() }}".to_string()),
|
when: Some("{{ succeeded() }}".to_string()),
|
||||||
publish: Vec::new(),
|
publish: Vec::new(),
|
||||||
r#do: Some(vec![target.clone()]),
|
r#do: Some(vec![target.clone()]),
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +291,7 @@ impl Task {
|
|||||||
when: Some("{{ failed() }}".to_string()),
|
when: Some("{{ failed() }}".to_string()),
|
||||||
publish: Vec::new(),
|
publish: Vec::new(),
|
||||||
r#do: Some(vec![target.clone()]),
|
r#do: Some(vec![target.clone()]),
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +301,7 @@ impl Task {
|
|||||||
when: None,
|
when: None,
|
||||||
publish: Vec::new(),
|
publish: Vec::new(),
|
||||||
r#do: Some(vec![target.clone()]),
|
r#do: Some(vec![target.clone()]),
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +310,7 @@ impl Task {
|
|||||||
when: Some("{{ timed_out() }}".to_string()),
|
when: Some("{{ timed_out() }}".to_string()),
|
||||||
publish: Vec::new(),
|
publish: Vec::new(),
|
||||||
r#do: Some(vec![target.clone()]),
|
r#do: Some(vec![target.clone()]),
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +320,7 @@ impl Task {
|
|||||||
when: branch.when.clone(),
|
when: branch.when.clone(),
|
||||||
publish: Vec::new(),
|
publish: Vec::new(),
|
||||||
r#do: Some(vec![branch.next.clone()]),
|
r#do: Some(vec![branch.next.clone()]),
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +338,7 @@ impl Task {
|
|||||||
when: Some("{{ succeeded() }}".to_string()),
|
when: Some("{{ succeeded() }}".to_string()),
|
||||||
publish: self.publish.clone(),
|
publish: self.publish.clone(),
|
||||||
r#do: None,
|
r#do: None,
|
||||||
|
chart_meta: None,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Attach to the first transition
|
// Attach to the first transition
|
||||||
@@ -1146,4 +1172,147 @@ tasks:
|
|||||||
Some(vec!["task2".to_string(), "task3".to_string()])
|
Some(vec!["task2".to_string(), "task3".to_string()])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chart_meta_roundtrip() {
|
||||||
|
// __chart_meta__ is frontend-only visual metadata that must survive
|
||||||
|
// a parse → serialize → parse round-trip so the workflow builder can
|
||||||
|
// restore node positions, edge colors, waypoints, etc.
|
||||||
|
let yaml = r##"
|
||||||
|
ref: test.chart_meta
|
||||||
|
label: Chart Meta Roundtrip
|
||||||
|
version: 1.0.0
|
||||||
|
tasks:
|
||||||
|
- name: task1
|
||||||
|
action: core.echo
|
||||||
|
__chart_meta__:
|
||||||
|
position:
|
||||||
|
x: 300
|
||||||
|
y: 120
|
||||||
|
next:
|
||||||
|
- when: "{{ succeeded() }}"
|
||||||
|
do:
|
||||||
|
- task2
|
||||||
|
__chart_meta__:
|
||||||
|
label: main path
|
||||||
|
color: "#22c55e"
|
||||||
|
line_style: dashed
|
||||||
|
edge_waypoints:
|
||||||
|
task2:
|
||||||
|
- x: 400
|
||||||
|
y: 200
|
||||||
|
label_positions:
|
||||||
|
task2: 0.35
|
||||||
|
- name: task2
|
||||||
|
action: core.echo
|
||||||
|
__chart_meta__:
|
||||||
|
position:
|
||||||
|
x: 300
|
||||||
|
y: 320
|
||||||
|
"##;
|
||||||
|
|
||||||
|
let workflow = parse_workflow_yaml(yaml).unwrap();
|
||||||
|
|
||||||
|
// Verify task-level __chart_meta__ was parsed
|
||||||
|
let task1_meta = workflow.tasks[0].chart_meta.as_ref().unwrap();
|
||||||
|
assert_eq!(task1_meta["position"]["x"], 300);
|
||||||
|
assert_eq!(task1_meta["position"]["y"], 120);
|
||||||
|
|
||||||
|
let task2_meta = workflow.tasks[1].chart_meta.as_ref().unwrap();
|
||||||
|
assert_eq!(task2_meta["position"]["x"], 300);
|
||||||
|
assert_eq!(task2_meta["position"]["y"], 320);
|
||||||
|
|
||||||
|
// Verify transition-level __chart_meta__ was parsed
|
||||||
|
let trans_meta = workflow.tasks[0].next[0].chart_meta.as_ref().unwrap();
|
||||||
|
assert_eq!(trans_meta["label"], "main path");
|
||||||
|
assert_eq!(trans_meta["color"].as_str().unwrap(), "#22c55e");
|
||||||
|
assert_eq!(trans_meta["line_style"], "dashed");
|
||||||
|
assert_eq!(trans_meta["edge_waypoints"]["task2"][0]["x"], 400);
|
||||||
|
assert_eq!(trans_meta["label_positions"]["task2"], 0.35);
|
||||||
|
|
||||||
|
// Round-trip through JSON serialization (simulates DB storage path)
|
||||||
|
let json = workflow_to_json(&workflow).unwrap();
|
||||||
|
let tasks = json["tasks"].as_array().unwrap();
|
||||||
|
|
||||||
|
// Task __chart_meta__ survives
|
||||||
|
assert_eq!(tasks[0]["__chart_meta__"]["position"]["x"], 300);
|
||||||
|
assert_eq!(tasks[1]["__chart_meta__"]["position"]["y"], 320);
|
||||||
|
|
||||||
|
// Transition __chart_meta__ survives
|
||||||
|
let next0 = &tasks[0]["next"].as_array().unwrap()[0];
|
||||||
|
assert_eq!(next0["__chart_meta__"]["label"], "main path");
|
||||||
|
assert_eq!(
|
||||||
|
next0["__chart_meta__"]["color"].as_str().unwrap(),
|
||||||
|
"#22c55e"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
next0["__chart_meta__"]["edge_waypoints"]["task2"][0]["x"],
|
||||||
|
400
|
||||||
|
);
|
||||||
|
assert_eq!(next0["__chart_meta__"]["label_positions"]["task2"], 0.35);
|
||||||
|
|
||||||
|
// Round-trip through YAML serialization (simulates file storage path)
|
||||||
|
let yaml_out = serde_yaml_ng::to_string(&workflow).unwrap();
|
||||||
|
let workflow2 = parse_workflow_yaml(&yaml_out).unwrap();
|
||||||
|
|
||||||
|
let task1_meta2 = workflow2.tasks[0].chart_meta.as_ref().unwrap();
|
||||||
|
assert_eq!(task1_meta2["position"]["x"], 300);
|
||||||
|
|
||||||
|
let trans_meta2 = workflow2.tasks[0].next[0].chart_meta.as_ref().unwrap();
|
||||||
|
assert_eq!(trans_meta2["label"], "main path");
|
||||||
|
assert_eq!(trans_meta2["color"].as_str().unwrap(), "#22c55e");
|
||||||
|
assert_eq!(trans_meta2["edge_waypoints"]["task2"][0]["x"], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chart_meta_absent_by_default() {
|
||||||
|
// Workflows without __chart_meta__ should parse fine with None values
|
||||||
|
let yaml = r#"
|
||||||
|
ref: test.no_meta
|
||||||
|
label: No Chart Meta
|
||||||
|
version: 1.0.0
|
||||||
|
tasks:
|
||||||
|
- name: task1
|
||||||
|
action: core.echo
|
||||||
|
next:
|
||||||
|
- when: "{{ succeeded() }}"
|
||||||
|
do:
|
||||||
|
- task2
|
||||||
|
- name: task2
|
||||||
|
action: core.echo
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let workflow = parse_workflow_yaml(yaml).unwrap();
|
||||||
|
assert!(workflow.tasks[0].chart_meta.is_none());
|
||||||
|
assert!(workflow.tasks[1].chart_meta.is_none());
|
||||||
|
assert!(workflow.tasks[0].next[0].chart_meta.is_none());
|
||||||
|
|
||||||
|
// Serialize to JSON and verify __chart_meta__ is omitted (not null)
|
||||||
|
let json = workflow_to_json(&workflow).unwrap();
|
||||||
|
let tasks = json["tasks"].as_array().unwrap();
|
||||||
|
assert!(tasks[0].get("__chart_meta__").is_none());
|
||||||
|
assert!(tasks[0]["next"][0].get("__chart_meta__").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_transitions_dont_gain_chart_meta() {
|
||||||
|
// Legacy format conversion should produce transitions without chart_meta
|
||||||
|
let yaml = r#"
|
||||||
|
ref: test.legacy_no_meta
|
||||||
|
label: Legacy No Meta
|
||||||
|
version: 1.0.0
|
||||||
|
tasks:
|
||||||
|
- name: task1
|
||||||
|
action: core.echo
|
||||||
|
on_success: task2
|
||||||
|
on_failure: task2
|
||||||
|
- name: task2
|
||||||
|
action: core.echo
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let workflow = parse_workflow_yaml(yaml).unwrap();
|
||||||
|
assert_eq!(workflow.tasks[0].next.len(), 2);
|
||||||
|
assert!(workflow.tasks[0].next[0].chart_meta.is_none());
|
||||||
|
assert!(workflow.tasks[0].next[1].chart_meta.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user