mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(graph): keep view state on page append and sanitize nebula exports
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user