feat(app): handle SQL export and tray updates
This commit is contained in:
parent
b01c17e709
commit
b2be49367c
|
|
@ -365,8 +365,7 @@ async function openSqlFile() {
|
|||
async function openSqlFilePath(path: string) {
|
||||
if (!isTauriRuntime()) return;
|
||||
try {
|
||||
const { readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const content = await readTextFile(path);
|
||||
const content = await api.readExternalSqlFile(path);
|
||||
const connectionId =
|
||||
connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || "";
|
||||
const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const tableError = ref<string | null>(null);
|
|||
const includeStructure = ref(true);
|
||||
const includeData = ref(true);
|
||||
const includeObjects = ref(true);
|
||||
const dropTableIfExists = ref(false);
|
||||
|
||||
// Export state
|
||||
const isExporting = ref(false);
|
||||
|
|
@ -197,6 +198,7 @@ async function startExport() {
|
|||
includeStructure: includeStructure.value,
|
||||
includeData: includeData.value,
|
||||
includeObjects: includeObjects.value,
|
||||
dropTableIfExists: dropTableIfExists.value,
|
||||
batchSize: 1000,
|
||||
};
|
||||
|
||||
|
|
@ -240,6 +242,7 @@ function resetState() {
|
|||
includeStructure.value = true;
|
||||
includeData.value = true;
|
||||
includeObjects.value = true;
|
||||
dropTableIfExists.value = false;
|
||||
isExporting.value = false;
|
||||
exportProgress.value = null;
|
||||
exportDone.value = false;
|
||||
|
|
@ -415,6 +418,15 @@ watch(open, async (val) => {
|
|||
<Square v-else class="w-3.5 h-3.5 text-muted-foreground/40 shrink-0" />
|
||||
{{ t("databaseExport.includeStructure") }}
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 text-xs"
|
||||
:class="includeStructure ? 'cursor-pointer' : 'cursor-not-allowed text-muted-foreground/50'"
|
||||
@click="includeStructure && (dropTableIfExists = !dropTableIfExists)"
|
||||
>
|
||||
<CheckSquare v-if="dropTableIfExists && includeStructure" class="w-3.5 h-3.5 text-primary shrink-0" />
|
||||
<Square v-else class="w-3.5 h-3.5 text-muted-foreground/40 shrink-0" />
|
||||
{{ t("databaseExport.dropTableIfExists") }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 cursor-pointer text-xs" @click="includeData = !includeData">
|
||||
<CheckSquare v-if="includeData" class="w-3.5 h-3.5 text-primary shrink-0" />
|
||||
<Square v-else class="w-3.5 h-3.5 text-muted-foreground/40 shrink-0" />
|
||||
|
|
|
|||
|
|
@ -1173,6 +1173,7 @@ export default {
|
|||
databaseExport: {
|
||||
title: "Export Database",
|
||||
includeStructure: "Table structure (DDL)",
|
||||
dropTableIfExists: "Add DROP TABLE IF EXISTS before DDL",
|
||||
includeData: "Table data (INSERT)",
|
||||
includeObjects: "Views / Procedures / Functions",
|
||||
tableSelection: "Tables",
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,7 @@ export default {
|
|||
databaseExport: {
|
||||
title: "Exportar base de datos",
|
||||
includeStructure: "Estructura de tablas (DDL)",
|
||||
dropTableIfExists: "Agregar DROP TABLE IF EXISTS antes del DDL",
|
||||
includeData: "Datos de tablas (INSERT)",
|
||||
includeObjects: "Vistas / Procedimientos / Funciones",
|
||||
tableSelection: "Tablas",
|
||||
|
|
|
|||
|
|
@ -1150,6 +1150,7 @@ export default {
|
|||
databaseExport: {
|
||||
title: "导出数据库",
|
||||
includeStructure: "表结构 (DDL)",
|
||||
dropTableIfExists: "导出前添加 DROP TABLE IF EXISTS",
|
||||
includeData: "表数据 (INSERT)",
|
||||
includeObjects: "视图 / 存储过程 / 函数",
|
||||
tableSelection: "选择表",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export const executeSqlFile = forward("executeSqlFile");
|
|||
export const cancelSqlFileExecution = forward("cancelSqlFileExecution");
|
||||
export const listenSqlFileProgress = forward("listenSqlFileProgress");
|
||||
export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles");
|
||||
export const readExternalSqlFile = forward("readExternalSqlFile");
|
||||
|
||||
// Data Transfer
|
||||
export const startTransfer = forward("startTransfer");
|
||||
|
|
|
|||
|
|
@ -482,6 +482,10 @@ export async function pendingOpenSqlFiles(): Promise<string[]> {
|
|||
return [];
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(_path: string): Promise<string> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data Transfer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@ export async function pendingOpenSqlFiles(): Promise<string[]> {
|
|||
return invoke("pending_open_sql_files");
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string): Promise<string> {
|
||||
return invoke("read_external_sql_file", { path });
|
||||
}
|
||||
|
||||
// --- AI Conversations ---
|
||||
|
||||
export interface AiChatMessage {
|
||||
|
|
@ -868,6 +872,7 @@ export interface DatabaseExportRequest {
|
|||
includeStructure: boolean;
|
||||
includeData: boolean;
|
||||
includeObjects: boolean;
|
||||
dropTableIfExists?: boolean;
|
||||
batchSize: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ pub struct DatabaseExportRequest {
|
|||
pub include_structure: bool,
|
||||
pub include_data: bool,
|
||||
pub include_objects: bool,
|
||||
#[serde(default)]
|
||||
pub drop_table_if_exists: bool,
|
||||
pub batch_size: usize,
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +161,10 @@ pub async fn export_database_sql_core(
|
|||
|
||||
// Export structure
|
||||
if request.include_structure {
|
||||
if request.drop_table_if_exists {
|
||||
writeln!(file, "{}\n", drop_table_if_exists_sql(table_name, &request.schema, &db_type))
|
||||
.map_err(|e| format!("Failed to write file: {e}"))?;
|
||||
}
|
||||
match crate::schema::get_table_ddl_core(
|
||||
state,
|
||||
&request.connection_id,
|
||||
|
|
@ -448,9 +454,14 @@ fn filter_selected_table_infos(
|
|||
tables.into_iter().filter(|table| selected.contains(table.name.as_str())).collect()
|
||||
}
|
||||
|
||||
fn drop_table_if_exists_sql(table_name: &str, schema: &str, db_type: &DatabaseType) -> String {
|
||||
format!("DROP TABLE IF EXISTS {};", crate::transfer::qualified_table(table_name, schema, db_type))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::filter_selected_table_infos;
|
||||
use super::{drop_table_if_exists_sql, filter_selected_table_infos};
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::types::TableInfo;
|
||||
|
||||
fn table(name: &str, table_type: &str) -> TableInfo {
|
||||
|
|
@ -474,4 +485,18 @@ mod tests {
|
|||
|
||||
assert_eq!(filtered.iter().map(|table| table.name.as_str()).collect::<Vec<_>>(), vec!["users", "orders"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_drop_table_if_exists_with_qualified_mysql_name() {
|
||||
let sql = drop_table_if_exists_sql("users", "app", &DatabaseType::Mysql);
|
||||
|
||||
assert_eq!(sql, "DROP TABLE IF EXISTS `app`.`users`;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_drop_table_if_exists_without_empty_schema() {
|
||||
let sql = drop_table_if_exists_sql("users", "", &DatabaseType::Postgres);
|
||||
|
||||
assert_eq!(sql, "DROP TABLE IF EXISTS \"users\";");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ tauri-build = { version = "2.5.6", features = [] }
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10.3", features = [] }
|
||||
tauri = { version = "2.10.3", features = ["tray-icon"] }
|
||||
tauri-plugin-log = "2"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "mysql", "postgres", "sqlite", "json", "chrono", "uuid", "rust_decimal"] }
|
||||
rust_decimal = { version = "1", features = ["serde"] }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) ->
|
|||
dedupe_paths(paths)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_external_sql_file(path: String) -> Result<String, String> {
|
||||
read_external_sql_file_content(Path::new(&path))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExternalSqlOpenState {
|
||||
pending: Mutex<Vec<String>>,
|
||||
|
|
@ -55,6 +60,13 @@ pub fn is_sql_file_path(path: &Path) -> bool {
|
|||
path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("sql")).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn read_external_sql_file_content(path: &Path) -> Result<String, String> {
|
||||
if !is_sql_file_path(path) {
|
||||
return Err("Only .sql files can be opened this way".to_string());
|
||||
}
|
||||
std::fs::read_to_string(path).map_err(|e| format!("Failed to read SQL file: {e}"))
|
||||
}
|
||||
|
||||
fn dedupe_paths(paths: Vec<String>) -> Vec<String> {
|
||||
let mut unique = Vec::new();
|
||||
for path in paths {
|
||||
|
|
@ -91,4 +103,26 @@ mod tests {
|
|||
assert_eq!(state.drain(), vec!["/tmp/a.sql"]);
|
||||
assert!(state.drain().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_external_sql_file_content() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
std::fs::write(&path, "select 1;").unwrap();
|
||||
|
||||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(result.unwrap(), "select 1;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_external_non_sql_file_content() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.txt", uuid::Uuid::new_v4()));
|
||||
std::fs::write(&path, "select 1;").unwrap();
|
||||
|
||||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert!(result.unwrap_err().contains(".sql"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,66 @@ use commands::connection::AppState;
|
|||
use dbx_core::storage::Storage;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tauri::{
|
||||
menu::MenuBuilder,
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
use tauri::{Emitter, Manager, RunEvent};
|
||||
|
||||
fn should_hide_window_on_close(target_os: &str) -> bool {
|
||||
matches!(target_os, "macos" | "windows")
|
||||
}
|
||||
|
||||
fn show_main_window(app: &tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn setup_windows_tray(app: &mut tauri::App) -> tauri::Result<()> {
|
||||
let menu = MenuBuilder::new(app).text("show", "Show DBX").separator().text("quit", "Quit DBX").build()?;
|
||||
let mut tray = TrayIconBuilder::with_id("main-tray").tooltip("DBX").menu(&menu).show_menu_on_left_click(false);
|
||||
|
||||
if let Some(icon) = app.default_window_icon().cloned() {
|
||||
tray = tray.icon(icon);
|
||||
}
|
||||
|
||||
tray.on_menu_event(|app, event| {
|
||||
if event.id() == "show" {
|
||||
show_main_window(app);
|
||||
} else if event.id() == "quit" {
|
||||
app.exit(0);
|
||||
}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| match event {
|
||||
TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. }
|
||||
| TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => show_main_window(tray.app_handle()),
|
||||
_ => {}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_hide_window_on_close;
|
||||
|
||||
#[test]
|
||||
fn hides_window_on_close_for_windows_and_macos() {
|
||||
assert!(should_hide_window_on_close("windows"));
|
||||
assert!(should_hide_window_on_close("macos"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_hide_window_on_close_for_other_platforms() {
|
||||
assert!(!should_hide_window_on_close("linux"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider");
|
||||
|
|
@ -75,15 +133,18 @@ pub fn run() {
|
|||
let _ = window.set_decorations(false);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
setup_windows_tray(app)?;
|
||||
window_state_guard::enforce_main_window_bounds(app.handle());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
if should_hide_window_on_close(std::env::consts::OS) {
|
||||
let _ = window.hide();
|
||||
api.prevent_close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
|
@ -135,6 +196,7 @@ pub fn run() {
|
|||
commands::sql_file::execute_sql_file,
|
||||
commands::sql_file::cancel_sql_file_execution,
|
||||
commands::external_sql::pending_open_sql_files,
|
||||
commands::external_sql::read_external_sql_file,
|
||||
commands::table_import::preview_table_import_file,
|
||||
commands::table_import::import_table_file,
|
||||
commands::table_import::cancel_table_import,
|
||||
|
|
@ -215,10 +277,7 @@ pub fn run() {
|
|||
#[cfg(target_os = "macos")]
|
||||
if let RunEvent::Reopen { has_visible_windows, .. } = &event {
|
||||
if !has_visible_windows {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
show_main_window(app_handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue