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:
2026-07-13 17:59:15 +02:00
parent 9326a4897a
commit afbf315e97
2 changed files with 86 additions and 0 deletions

View File

@ -2336,6 +2336,18 @@ pub struct CloseViewWindowResponseDto {
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`.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
@ -2429,6 +2441,19 @@ pub(crate) fn view_window_url(panel: ViewPanel) -> String {
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(
app: &AppHandle,
kind: &'static str,
@ -2468,6 +2493,30 @@ pub async fn 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.
///
/// 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"));
}
#[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]
fn view_window_lifecycle_payload_is_camel_case() {
let payload = ViewWindowLifecycleEventDto {

View File

@ -264,6 +264,7 @@ pub fn run() {
commands::resolve_memory_links,
commands::set_focused_project,
commands::get_focused_project,
commands::list_open_view_windows,
commands::open_view_window,
commands::close_view_window,
commands::move_tab_to_new_window,