feat(sidebar): add nested virtual table groups with drag and batch move

This commit is contained in:
ReadMe.md authored and GitHub committed 2026-09-19 09:24:34 +08:00
1 parent 0db9756a57
commit 1f43d184aa
31 files changed
+1714 -20

No files matched your search

@@ -775,6 +775,10 @@ const SCHEMA_STATEMENTS: &[&str] = &[
id INTEGER PRIMARY KEY CHECK (id = 1),
layout_json TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS table_vgroups (
scope_key TEXT PRIMARY KEY,
layout_json TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS app_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
settings_json TEXT NOT NULL
@@ -4475,6 +4479,55 @@ impl Storage {
.await?;
json.map(|value| serde_json::from_str(&value).map_err(|e| e.to_string())).transpose()
}
pub async fn save_table_vgroups(&self, scope_key: &str, layout: &serde_json::Value) -> Result<(), String> {
let scope_key = scope_key.to_string();
let json = serde_json::to_string(layout).map_err(|e| e.to_string())?;
self.with_conn(move |conn| {
conn.execute(
"INSERT OR REPLACE INTO table_vgroups (scope_key, layout_json) VALUES (?1, ?2)",
params![scope_key, json],
)
.map(|_| ())
.map_err(|e| e.to_string())
})
.await
}
pub async fn load_table_vgroups(&self) -> Result<serde_json::Value, String> {
self.with_conn(|conn| {
let mut stmt =
conn.prepare("SELECT scope_key, layout_json FROM table_vgroups").map_err(|e| e.to_string())?;
let rows = stmt
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
.map_err(|e| e.to_string())?;
let mut layouts = serde_json::Map::new();
for row in rows {
let (scope_key, json) = row.map_err(|e| e.to_string())?;
match serde_json::from_str::<serde_json::Value>(&json) {
Ok(layout) => {
layouts.insert(scope_key, layout);
}
Err(e) => warn!("Failed to deserialize table vgroups for scope {scope_key}: {e}"),
}
}
Ok(serde_json::Value::Object(layouts))
})
.await
}
/// 删除连接时清理其名下全部表分组布局(scope_key 前缀 = `{connectionId}\u{0}`)。
pub async fn delete_table_vgroups_for_connection(&self, connection_id: &str) -> Result<(), String> {
// 前缀用 instr 做大小写敏感的字节匹配:LIKE 默认大小写不敏感且把 `%`/`_` 当通配符,
// 会连带删除 id 仅大小写不同或含通配符的连接行。
let prefix = format!("{connection_id}\u{0}");
self.with_conn(move |conn| {
conn.execute("DELETE FROM table_vgroups WHERE instr(scope_key, ?1) = 1", params![prefix])
.map(|_| ())
.map_err(|e| e.to_string())
})
.await
}
}
// Schema cache
@@ -5434,6 +5487,38 @@ mod tests {
dir
}
#[tokio::test]
async fn delete_table_vgroups_for_connection_scopes_by_prefix() {
let path = temp_db_path("vgroup-delete");
let storage = Storage::open(&path).await.unwrap();
let layout = serde_json::json!({ "version": 1, "groups": [], "order": [] });
// 真实 scope_key 形如 `{connectionId}\u{0}{linkedServer}\u{0}{catalog}\u{0}{database}\u{0}{schema}`。
storage.save_table_vgroups("conn-1\u{0}\u{0}\u{0}db1\u{0}", &layout).await.unwrap();
storage.save_table_vgroups("conn-1\u{0}\u{0}\u{0}db2\u{0}", &layout).await.unwrap();
storage.save_table_vgroups("conn-2\u{0}\u{0}\u{0}db1\u{0}", &layout).await.unwrap();
// 前缀恰好是另一连接 id 的连接不能被误删。
storage.save_table_vgroups("conn-12\u{0}\u{0}\u{0}db1\u{0}", &layout).await.unwrap();
// 仅大小写不同、或 id 含 LIKE 通配符的连接同样不能被误删。
storage.save_table_vgroups("CONN-1\u{0}\u{0}\u{0}db1\u{0}", &layout).await.unwrap();
storage.save_table_vgroups("conn_1\u{0}\u{0}\u{0}db1\u{0}", &layout).await.unwrap();
storage.delete_table_vgroups_for_connection("conn-1").await.unwrap();
let remaining = storage.load_table_vgroups().await.unwrap();
let mut keys: Vec<&String> = remaining.as_object().unwrap().keys().collect();
keys.sort();
assert_eq!(
keys,
vec![
"CONN-1\u{0}\u{0}\u{0}db1\u{0}",
"conn-12\u{0}\u{0}\u{0}db1\u{0}",
"conn-2\u{0}\u{0}\u{0}db1\u{0}",
"conn_1\u{0}\u{0}\u{0}db1\u{0}",
]
);
}
#[cfg(unix)]
fn file_mode(path: &std::path::Path) -> Option<u32> {
use std::os::unix::fs::PermissionsExt;