fix(graph): keep view state on page append and sanitize nebula exports

This commit is contained in:
t8y2
2026-10-08 18:32:03 +08:00
parent f549366212
commit 96ff392e40
3 changed files with 85 additions and 6 deletions
@@ -291,11 +291,25 @@ function exportImage() {
watch(query, highlightSearch);
watch(
() => props.graph,
() => {
expanded.value = undefined;
hidden.value = new Set();
pinned.value = new Set();
void nextTick(() => renderGraph());
(next, previous) => {
// Appending a result page reuses the same cells and only adds new ones;
// keep the view state and re-render with preserved positions instead of
// wiping selection/pins/hidden nodes and re-running a full layout.
const appended =
!!previous &&
previous.cells.length < next.cells.length &&
(() => {
const nextCellKeys = new Set(next.cells.map((cell) => `${cell.row}:${cell.column}`));
if (previous.cells.some((cell) => !nextCellKeys.has(`${cell.row}:${cell.column}`))) return false;
const nextNodeIds = new Set(next.nodes.map((node) => node.id));
return previous.nodes.every((node) => nextNodeIds.has(node.id));
})();
if (!appended) {
expanded.value = undefined;
hidden.value = new Set();
pinned.value = new Set();
}
void nextTick(() => renderGraph(appended));
},
);
watch(selectedId, () => {
@@ -1100,6 +1100,9 @@ async fn export_query_result_core_inner(
result.rows.truncate(this_page);
}
let row_count = result.rows.len();
if request.database_type == DatabaseType::Nebula {
crate::text_export::normalize_nebula_export_rows(&mut result.rows);
}
let formatted_rows = crate::temporal_format::format_temporal_export_rows_with_string_types_cow(
&result.rows,
&page_column_types,
+63 -1
View File
@@ -12,6 +12,20 @@ pub struct QueryResultTextExportData {
pub rows: Vec<Vec<Value>>,
}
pub fn normalize_nebula_export_rows(rows: &mut [Vec<Value>]) {
for value in rows.iter_mut().flatten() {
let graph_cell = value.get("__dbx_graph_cell").and_then(Value::as_str) == Some("nebula-v1")
&& value.get("kind").is_some_and(Value::is_string)
&& value.get("nodes").is_some_and(Value::is_array)
&& value.get("edges").is_some_and(Value::is_array);
if graph_cell {
if let Some(display) = value.get("display").and_then(Value::as_str) {
*value = Value::String(display.to_owned());
}
}
}
}
pub fn format_json(data: &QueryResultTextExportData) -> Result<String, String> {
let rows = data
.rows
@@ -428,7 +442,55 @@ fn chrono_local_now() -> String {
mod tests {
use serde_json::{json, Value};
use super::{format_html, format_json, format_markdown, QueryResultTextExportData};
use super::{format_html, format_json, format_markdown, normalize_nebula_export_rows, QueryResultTextExportData};
#[test]
fn nebula_exports_use_display_cells_without_leaking_graph_metadata() {
let mut rows = vec![
vec![
json!({
"__dbx_graph_cell": "nebula-v1", "kind": "vertex", "display": "(:\"player\" {\"name\":\"Tim\"})",
"nodes": [{"id": "vid:1", "labels": ["player"], "properties": [{"name": "name", "value": "Tim"}]}], "edges": []
}),
json!(false),
Value::Null,
],
vec![
json!({"__dbx_graph_cell": "nebula-v1", "kind": "edge", "display": "[:follow]", "nodes": [], "edges": [{"type": "follow"}]}),
json!(7),
json!("plain"),
],
];
normalize_nebula_export_rows(&mut rows);
assert_eq!(rows[0], vec![json!("(:\"player\" {\"name\":\"Tim\"})"), json!(false), Value::Null]);
assert_eq!(rows[1], vec![json!("[:follow]"), json!(7), json!("plain")]);
let data = QueryResultTextExportData {
title: None,
columns: vec!["entity".into(), "value".into(), "note".into()],
rows,
};
let exported: Value = serde_json::from_str(&format_json(&data).unwrap()).unwrap();
assert_eq!(exported[0]["entity"], json!("(:\"player\" {\"name\":\"Tim\"})"));
assert!(!format_markdown(&data).contains("__dbx_graph_cell"));
}
#[test]
fn nebula_export_normalization_preserves_ordinary_and_unknown_objects() {
let mut rows = vec![vec![
json!({"display": "ordinary", "properties": []}),
json!({"__dbx_graph_cell": "neo4j-v1", "kind": "vertex", "display": "neo4j", "nodes": [], "edges": []}),
json!({"__dbx_graph_cell": "nebula-v2", "kind": "vertex", "display": "future", "nodes": [], "edges": []}),
json!({"__dbx_graph_cell": "nebula-v1", "kind": "vertex", "display": 42, "nodes": [], "edges": []}),
json!({"__dbx_graph_cell": "nebula-v1", "kind": "vertex", "display": "malformed", "nodes": null, "edges": []}),
]];
let original = rows.clone();
normalize_nebula_export_rows(&mut rows);
assert_eq!(rows[0][0], json!({"display": "ordinary", "properties": []}));
assert_eq!(rows[0][1], original[0][1]);
assert_eq!(rows[0][2], original[0][2]);
assert_eq!(rows[0][3], original[0][3]);
assert_eq!(rows[0][4], original[0][4]);
}
#[test]
fn formats_json_rows_as_objects() {