v0.7.8: MySQL/SQLite backup-sync, Excel export, query cancel, settings import/export, Windows title-bar fix, SQLite table editor (#16)
* [P1-T1] feat(backup): MySQL/SQLite backup models + db_type on SyncOptions (Task 1.1) * [P1-T2] feat: enable tools(mysql,sqlite) + tableManagement(sqlite) + add fflate (Task 1.2) * [P1-T3] feat(cancel): CancelHandle enum + CancelRegistry (Task 1.3) * [P2-T1] feat(export): hand-rolled XLSX writer with inline-string cells (Task 2.1) * [P2-T2] feat(backup): SQLite .dump/restore/sync core, fail-closed virtual tables (Task 2.2) * [P2-T3] feat(backup): MySQL dump/restore/sync arg builders + tool resolution (Task 2.3) * [P2-T4] feat(settings): pure import validator + SettingsExport envelope + Store.apply_settings (Task 2.4) * [P2-T5] feat(table-editor): SQLite create/diff/rebuild SQL generation, fail-closed AUTOINCREMENT (Task 2.5) * [P3-T1] feat(commands): MySQL/SQLite backup + settings export/import commands + wrappers (Task 3.1) * [P3-T2] feat(cancel): capture cancel primitives at connect; cancel_query command; SQLite interrupt test (Task 3.2) * [P3-T3] feat(table-editor): SQLite object-change dispatch + execute_change Ddl/RebuildTable (Task 3.3) * [P4-T1] feat(tools): DB-aware backup/restore/sync pages (Task 4.1) * [P4-T2] feat(export): xlsx export in grid toolbar + overflow menu (Task 4.2) * [P4-T3] feat(query): cancel button wired to cancelQuery (Task 4.3) * [P4-T4] feat(settings): export/import buttons + validation gate (Task 4.4) * [P4-T5] fix(ui): gate macOS overlay drag strip to macOS only (Task 4.5) * [P4-T6] feat(table-editor): SQLite Create/Edit Table mode (Task 4.6) * [P5-T1] chore: bump 0.7.7 -> 0.7.8 + README/AGENTS/ROADMAP status (Task 5.1) * [P5-T2] build(release): bundle mariadb-dump + mariadb client (system-first fallback) (Task 5.2) * fix(cancel): propagate cancellations past wrapped->raw fallback (SQLite/PG/MySQL) + MySQL CONNECTION_ID cast * fix(export): Excel export from overflow menu did nothing + add export success/error toasts * fix(export): tree kebab export fetches table data when rows not loaded * docs(readme): surface v0.7.8 features (MySQL/SQLite backup-sync, Excel export, query cancel, SQLite table editor, settings import/export)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::models::Settings;
|
||||
use crate::store::Store;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub fn get_settings_inner(state: &Mutex<Store>) -> Result<Settings, String> {
|
||||
@@ -26,6 +27,68 @@ pub fn update_setting(
|
||||
update_setting_inner(&state.db_store, &key, &value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings export / import (v0.7.8)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Flatten a `Settings` struct into the store's key/value map. Keys and value
|
||||
/// formats must round-trip through `Store::get_settings` (e.g. a `None`
|
||||
/// `default_folder_id` is stored as the literal `"null"` sentinel, which
|
||||
/// `get_settings` filters back to `None`).
|
||||
fn settings_to_kv(s: &crate::models::Settings) -> HashMap<String, String> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("confirm_before_delete".into(), s.confirm_before_delete.to_string());
|
||||
if let Some(f) = &s.default_folder_id {
|
||||
m.insert("default_folder_id".into(), f.clone());
|
||||
} else {
|
||||
m.insert("default_folder_id".into(), "null".into());
|
||||
}
|
||||
m.insert("theme".into(), s.theme.clone());
|
||||
m.insert("font_size".into(), s.font_size.clone());
|
||||
m.insert("accent_color".into(), s.accent_color.clone());
|
||||
m.insert("table_refresh_rate".into(), s.table_refresh_rate.to_string());
|
||||
m.insert("table_page_size".into(), s.table_page_size.to_string());
|
||||
m.insert("editor_font_size".into(), s.editor_font_size.to_string());
|
||||
m.insert("editor_font_family".into(), s.editor_font_family.clone());
|
||||
m.insert("editor_word_wrap".into(), s.editor_word_wrap.clone());
|
||||
m.insert("editor_minimap".into(), s.editor_minimap.to_string());
|
||||
m.insert("editor_tab_size".into(), s.editor_tab_size.to_string());
|
||||
if let Some(o) = &s.tag_order {
|
||||
m.insert("tag_order".into(), o.clone());
|
||||
}
|
||||
m.insert(
|
||||
"default_ports".into(),
|
||||
serde_json::to_string(&s.default_ports).unwrap_or_default(),
|
||||
);
|
||||
m.insert(
|
||||
"shortcuts".into(),
|
||||
serde_json::to_string(&s.shortcuts).unwrap_or_default(),
|
||||
);
|
||||
m
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn export_settings(state: tauri::State<crate::AppState>) -> Result<String, String> {
|
||||
let s = get_settings_inner(&state.db_store)?;
|
||||
serde_json::to_string(&crate::models::settings::SettingsExport {
|
||||
schema_version: 1,
|
||||
settings: s,
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn import_settings(
|
||||
json: String,
|
||||
state: tauri::State<crate::AppState>,
|
||||
) -> Result<(), String> {
|
||||
let env: crate::models::settings::SettingsExport =
|
||||
serde_json::from_str(&json).map_err(|e| format!("invalid settings file: {e}"))?;
|
||||
let map = settings_to_kv(&env.settings);
|
||||
let store = state.db_store.lock().map_err(|e| e.to_string())?;
|
||||
store.apply_settings(&map)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -59,4 +122,31 @@ mod tests {
|
||||
update_setting_inner(&st, "accent_color", "#EF4444").unwrap();
|
||||
assert_eq!(get_settings_inner(&st).unwrap().accent_color, "#EF4444");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_export_envelope_camel_case() {
|
||||
use std::collections::HashMap;
|
||||
let env = crate::models::settings::SettingsExport { schema_version: 1, settings: crate::models::Settings {
|
||||
confirm_before_delete: true, default_folder_id: None, theme: "dark".into(), font_size: "medium".into(),
|
||||
default_ports: HashMap::new(), tag_order: None, table_refresh_rate: 5, table_page_size: 50,
|
||||
shortcuts: HashMap::new(), accent_color: "#2563EB".into(), editor_font_size: 14,
|
||||
editor_font_family: "Menlo".into(), editor_word_wrap: "off".into(), editor_minimap: true, editor_tab_size: 2,
|
||||
}};
|
||||
let json = serde_json::to_string(&env).unwrap();
|
||||
assert!(json.contains("\"schemaVersion\":1"));
|
||||
assert!(json.contains("\"settings\":"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_apply_settings_writes_all_keys() {
|
||||
let conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
crate::store::migrations::run_migrations(&conn).unwrap();
|
||||
let store = crate::store::Store::from_connection(conn);
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert("theme".to_string(), "light".to_string());
|
||||
map.insert("accent_color".to_string(), "#EF4444".to_string());
|
||||
store.apply_settings(&map).unwrap();
|
||||
assert_eq!(store.get_settings().unwrap().theme, "light");
|
||||
assert_eq!(store.get_settings().unwrap().accent_color, "#EF4444");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user