feat(view-windows): exposer list_open_view_windows pour l'état du menu
Ajoute la commande Tauri `list_open_view_windows` qui retourne un `ViewWindowSnapshot` (panel, label, visible) par fenêtre de panneau détachée, avec le parseur `view_panel_from_window_label` (labels stables et legacy suffixés du project id). Enregistre la commande dans le handler. Backend seul pour le ticket #50 ; pas encore de consommateur frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2336,6 +2336,18 @@ pub struct CloseViewWindowResponseDto {
|
|||||||
pub closed: bool,
|
pub closed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snapshot returned by `list_open_view_windows`.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ViewWindowSnapshot {
|
||||||
|
/// Panel id rendered by the detached window.
|
||||||
|
pub panel: String,
|
||||||
|
/// Stable Tauri window label.
|
||||||
|
pub label: String,
|
||||||
|
/// Whether Tauri currently reports the OS window as visible.
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Payload emitted on `view-window://lifecycle`.
|
/// Payload emitted on `view-window://lifecycle`.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -2429,6 +2441,19 @@ pub(crate) fn view_window_url(panel: ViewPanel) -> String {
|
|||||||
format!("index.html?panel={}", panel.as_str())
|
format!("index.html?panel={}", panel.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn view_panel_from_window_label(label: &str) -> Option<ViewPanel> {
|
||||||
|
let rest = label.strip_prefix("view-")?;
|
||||||
|
if let Ok(panel) = ViewPanel::parse(rest) {
|
||||||
|
return Some(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (panel, project_id) = rest.rsplit_once('-')?;
|
||||||
|
if project_id.len() != 32 || uuid::Uuid::parse_str(project_id).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
ViewPanel::parse(panel).ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn emit_view_window_lifecycle(
|
pub(crate) fn emit_view_window_lifecycle(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
@ -2468,6 +2493,30 @@ pub async fn get_focused_project(
|
|||||||
Ok(state.get_focused_project())
|
Ok(state.get_focused_project())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `list_open_view_windows` — return the detached panel windows currently
|
||||||
|
/// registered by Tauri.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_open_view_windows(app: AppHandle) -> Vec<ViewWindowSnapshot> {
|
||||||
|
let mut windows = app
|
||||||
|
.webview_windows()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(label, window)| {
|
||||||
|
let panel = view_panel_from_window_label(&label)?;
|
||||||
|
Some(ViewWindowSnapshot {
|
||||||
|
panel: panel.as_str().to_owned(),
|
||||||
|
label,
|
||||||
|
visible: window.is_visible().unwrap_or(true),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
windows.sort_by(|left, right| {
|
||||||
|
left.panel
|
||||||
|
.cmp(&right.panel)
|
||||||
|
.then(left.label.cmp(&right.label))
|
||||||
|
});
|
||||||
|
windows
|
||||||
|
}
|
||||||
|
|
||||||
/// `open_view_window` — open or focus a detached OS window for one panel.
|
/// `open_view_window` — open or focus a detached OS window for one panel.
|
||||||
///
|
///
|
||||||
/// The window is a normal decorated, resizable system window. Its webview loads
|
/// The window is a normal decorated, resizable system window. Its webview loads
|
||||||
@ -2578,6 +2627,42 @@ mod view_window_tests {
|
|||||||
assert!(err.message.contains("invalid view panel: terminal"));
|
assert!(err.message.contains("invalid view panel: terminal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_window_label_parser_accepts_stable_and_legacy_labels() {
|
||||||
|
assert_eq!(
|
||||||
|
view_panel_from_window_label("view-tickets")
|
||||||
|
.unwrap()
|
||||||
|
.as_str(),
|
||||||
|
"tickets"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view_panel_from_window_label("view-tickets-0000000000000000000000000000002a")
|
||||||
|
.unwrap()
|
||||||
|
.as_str(),
|
||||||
|
"tickets"
|
||||||
|
);
|
||||||
|
assert!(view_panel_from_window_label("main").is_none());
|
||||||
|
assert!(view_panel_from_window_label("view-unknown").is_none());
|
||||||
|
assert!(view_panel_from_window_label("view-tickets-not-a-project").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_window_snapshot_payload_is_camel_case() {
|
||||||
|
let payload = ViewWindowSnapshot {
|
||||||
|
panel: "tickets".to_owned(),
|
||||||
|
label: "view-tickets".to_owned(),
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&payload).unwrap();
|
||||||
|
assert!(json.contains("\"panel\":\"tickets\""), "json was {json}");
|
||||||
|
assert!(
|
||||||
|
json.contains("\"label\":\"view-tickets\""),
|
||||||
|
"json was {json}"
|
||||||
|
);
|
||||||
|
assert!(json.contains("\"visible\":true"), "json was {json}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn view_window_lifecycle_payload_is_camel_case() {
|
fn view_window_lifecycle_payload_is_camel_case() {
|
||||||
let payload = ViewWindowLifecycleEventDto {
|
let payload = ViewWindowLifecycleEventDto {
|
||||||
|
|||||||
@ -264,6 +264,7 @@ pub fn run() {
|
|||||||
commands::resolve_memory_links,
|
commands::resolve_memory_links,
|
||||||
commands::set_focused_project,
|
commands::set_focused_project,
|
||||||
commands::get_focused_project,
|
commands::get_focused_project,
|
||||||
|
commands::list_open_view_windows,
|
||||||
commands::open_view_window,
|
commands::open_view_window,
|
||||||
commands::close_view_window,
|
commands::close_view_window,
|
||||||
commands::move_tab_to_new_window,
|
commands::move_tab_to_new_window,
|
||||||
|
|||||||
Reference in New Issue
Block a user