feat(tabs): detach SQL and data tabs into separate windows
This commit is contained in:
1 parent
717dcc1294
commit
d38be39e03
17 files changed
+957
-21
No files matched your search
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use dbx_core::storage::{DesktopSettings, McpGlobalPolicy, McpGlobalPolicyState};
|
||||
@@ -13,6 +14,30 @@ use crate::{
|
||||
};
|
||||
|
||||
const DEVELOPMENT_OPEN_TABS_STATE_KEY: &str = "development_open_tabs";
|
||||
const DETACHED_TABS_STATE_KEY: &str = "detached_tabs";
|
||||
static DETACHED_TABS_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
|
||||
static APPROVED_DETACHED_CLOSES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
|
||||
fn detached_tabs_lock() -> &'static tokio::sync::Mutex<()> {
|
||||
DETACHED_TABS_LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
|
||||
}
|
||||
|
||||
fn approved_detached_closes() -> &'static Mutex<HashSet<String>> {
|
||||
APPROVED_DETACHED_CLOSES.get_or_init(|| Mutex::new(HashSet::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn take_approved_detached_window_close(label: &str) -> bool {
|
||||
approved_detached_closes().lock().map(|mut labels| labels.remove(label)).unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn load_detached_tabs_map(state: &AppState) -> Result<serde_json::Map<String, serde_json::Value>, String> {
|
||||
Ok(state
|
||||
.storage
|
||||
.load_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY)
|
||||
.await?
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn open_tabs_state_key(debug_build: bool) -> &'static str {
|
||||
if debug_build {
|
||||
@@ -161,6 +186,53 @@ pub async fn save_open_tabs_state(state: State<'_, Arc<AppState>>, payload: serd
|
||||
state.storage.save_open_tabs_state_with_key(open_tabs_state_key(cfg!(debug_assertions)), &payload).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_detached_tab_handoff(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
tab_id: String,
|
||||
handoff: serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
let _guard = detached_tabs_lock().lock().await;
|
||||
let mut tabs = load_detached_tabs_map(state.inner()).await?;
|
||||
tabs.insert(tab_id, handoff);
|
||||
state.storage.save_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY, &serde_json::Value::Object(tabs)).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_detached_tab_handoff(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
tab_id: String,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
let _guard = detached_tabs_lock().lock().await;
|
||||
Ok(load_detached_tabs_map(state.inner()).await?.remove(&tab_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_detached_tab_handoffs(state: State<'_, Arc<AppState>>) -> Result<Vec<serde_json::Value>, String> {
|
||||
let _guard = detached_tabs_lock().lock().await;
|
||||
Ok(load_detached_tabs_map(state.inner()).await?.into_values().collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_detached_tab_handoff(state: State<'_, Arc<AppState>>, tab_id: String) -> Result<(), String> {
|
||||
let _guard = detached_tabs_lock().lock().await;
|
||||
let mut tabs = load_detached_tabs_map(state.inner()).await?;
|
||||
tabs.remove(&tab_id);
|
||||
state.storage.save_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY, &serde_json::Value::Object(tabs)).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn approve_detached_window_close(window: Window) -> Result<(), String> {
|
||||
if !window.label().starts_with("detached-tab-") {
|
||||
return Err("current window is not a detached tab window".to_string());
|
||||
}
|
||||
approved_detached_closes()
|
||||
.lock()
|
||||
.map_err(|_| "detached window close state is unavailable".to_string())?
|
||||
.insert(window.label().to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_saved_sql_editor_positions(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
||||
@@ -1697,7 +1697,24 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
|
||||
let _ = window.emit("dbx:detached-tab-lost", serde_json::json!({ "tabId": tab_id }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
|
||||
if commands::app_settings::take_approved_detached_window_close(window.label()) {
|
||||
return;
|
||||
}
|
||||
api.prevent_close();
|
||||
// Broadcast with the tabId payload: JS listeners registered
|
||||
// with the default `listen()` target receive events emitted
|
||||
// to any window, so the frontend must filter by tabId.
|
||||
let _ = window.emit("dbx:detached-tab-close-requested", serde_json::json!({ "tabId": tab_id }));
|
||||
return;
|
||||
}
|
||||
if !should_hide_window_on_close(std::env::consts::OS) {
|
||||
return;
|
||||
}
|
||||
@@ -1768,6 +1785,11 @@ pub fn run() {
|
||||
commands::app_settings::save_editor_settings,
|
||||
commands::app_settings::load_open_tabs_state,
|
||||
commands::app_settings::save_open_tabs_state,
|
||||
commands::app_settings::save_detached_tab_handoff,
|
||||
commands::app_settings::load_detached_tab_handoff,
|
||||
commands::app_settings::list_detached_tab_handoffs,
|
||||
commands::app_settings::delete_detached_tab_handoff,
|
||||
commands::app_settings::approve_detached_window_close,
|
||||
commands::app_settings::load_saved_sql_editor_positions,
|
||||
commands::app_settings::save_saved_sql_editor_positions,
|
||||
commands::app_settings::load_transfer_task_library,
|
||||
|
||||
Reference in new issue
Block a user