diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b45b207..f6e18a5 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2008,26 +2008,44 @@ pub async fn delete_assets_bulk( pub async fn export_assets( path: String, format: String, + // Both default to false → live-only export (existing behavior). + // include_deleted = true → live + deleted rows in one list + // only_deleted = true → deleted rows only (implies include) + // The UI drives these from the Visibility panel's own toggles so the + // export scope always matches what the user is looking at. + #[allow(non_snake_case)] include_deleted: Option, + #[allow(non_snake_case)] only_deleted: Option, state: State<'_, AppState>, ) -> Result { - let assets: Vec = sqlx::query_as( + let include_deleted = include_deleted.unwrap_or(false); + let only_deleted = only_deleted.unwrap_or(false); + let where_clause = if only_deleted { + "WHERE deleted = 1" + } else if include_deleted { + "WHERE 1 = 1" + } else { + "WHERE deleted = 0" + }; + let sql = format!( "SELECT id, row_id, asset_type, asset_name, video_name, lat, lng, start_lat, start_lng, end_lat, end_lng, image_path, image_path1, image_path2, image_path3, deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked - FROM assets WHERE deleted = 0 ORDER BY asset_type, asset_name, id", - ) - .fetch_all(&state.pool) - .await - .map_err(|e| e.to_string())?; + FROM assets {} ORDER BY asset_type, asset_name, id", + where_clause + ); + let assets: Vec = sqlx::query_as(&sql) + .fetch_all(&state.pool) + .await + .map_err(|e| e.to_string())?; // Resolve link partner ids → row_ids. The raw `link_pair_id` is an // internal autoincrement that changes on every re-import, so a CSV // consumer can't tell who linked to whom by looking at the number. // Look up the partner's row_id once and surface it as a parallel column // ("link_pair_row_id") in every format. Falls back to None when the - // partner row is deleted (and thus not in `assets`) — flagged so the - // user can see the dangling link. + // partner row isn't in the exported set (e.g. deleted while we're + // exporting live-only) — flagged so the user can see the dangling link. let row_id_by_id: std::collections::HashMap = assets .iter() .map(|a| (a.id, a.row_id.clone())) @@ -2036,14 +2054,16 @@ pub async fn export_assets( pid.and_then(|p| row_id_by_id.get(&p).cloned()) }; - // Pre-fetch all bboxes once so every format branch that wants to surface - // them can do so without re-querying. Grouped by asset_id → slot. - let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as( + // Pre-fetch all bboxes for the rows we're exporting. Match the same + // deleted-scope so a deleted row's bboxes ride along when it does. + let bbox_sql = format!( "SELECT b.asset_id, b.slot, b.x1, b.y1, b.x2, b.y2 FROM asset_bboxes b JOIN assets a ON a.id = b.asset_id - WHERE a.deleted = 0", - ) + {}", + where_clause.replace("deleted", "a.deleted") + ); + let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as(&bbox_sql) .fetch_all(&state.pool) .await .map_err(|e| e.to_string())?; @@ -2213,8 +2233,14 @@ pub async fn export_assets( map.insert(format!("{prefix}bbox_y2"), serde_json::json!(bb.3)); } }; + // Bucket into four lists so the envelope can split live from + // deleted rows explicitly. Downstream scripts that only care + // about live data ignore the `deleted_*` keys; audit tools read + // them. Empty lists are omitted from the envelope. let mut fixed_arr: Vec = vec![]; let mut range_arr: Vec = vec![]; + let mut deleted_fixed: Vec = vec![]; + let mut deleted_range: Vec = vec![]; for a in &assets { if a.asset_type == "fixed" { if let (Some(la), Some(ln)) = (a.lat, a.lng) { @@ -2235,7 +2261,11 @@ pub async fn export_assets( inject_bbox(&mut obj, "", bb); } } - fixed_arr.push(obj); + if a.deleted != 0 { + deleted_fixed.push(obj); + } else { + fixed_arr.push(obj); + } } } else if a.asset_type == "range" { if let (Some(la), Some(ln), Some(sla), Some(sln), Some(ela), Some(eln)) = @@ -2271,15 +2301,25 @@ pub async fn export_assets( inject_bbox(&mut obj, "end_", bb); } } - range_arr.push(obj); + if a.deleted != 0 { + deleted_range.push(obj); + } else { + range_arr.push(obj); + } } } } - serde_json::to_string_pretty(&serde_json::json!({ - "fixed": fixed_arr, - "range": range_arr, - })) - .map_err(|e| e.to_string())? + let mut envelope = serde_json::Map::new(); + envelope.insert("fixed".into(), serde_json::json!(fixed_arr)); + envelope.insert("range".into(), serde_json::json!(range_arr)); + if !deleted_fixed.is_empty() { + envelope.insert("deleted_fixed".into(), serde_json::json!(deleted_fixed)); + } + if !deleted_range.is_empty() { + envelope.insert("deleted_range".into(), serde_json::json!(deleted_range)); + } + serde_json::to_string_pretty(&serde_json::Value::Object(envelope)) + .map_err(|e| e.to_string())? } "csv" => { let mut s = String::new(); diff --git a/src/App.tsx b/src/App.tsx index 66961fd..f583af9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3423,12 +3423,24 @@ function AppShell({ outPath = `${outPath}.${wantExt}`; } setBusy(true); + // Match the Visibility panel's own toggles so what's exported mirrors + // what the user is looking at on the map: + // Only deleted → deleted rows only + // Show deleted → live + deleted in one export + // (neither on) → live rows only (default) const count = await invoke("export_assets", { path: outPath, format: exportFormat, + includeDeleted: showDeleted, + onlyDeleted: onlyDeleted, }); - setStatus(`Exported ${count} asset(s) to ${outPath}`); - showToast(`Exported ${count} asset(s) (${exportFormat})`); + const scopeLabel = onlyDeleted + ? "deleted-only" + : showDeleted + ? "live + deleted" + : "live"; + setStatus(`Exported ${count} asset(s) to ${outPath} (${scopeLabel})`); + showToast(`Exported ${count} asset(s) (${exportFormat}, ${scopeLabel})`); } catch (e) { setStatus(`Export failed: ${e}`); showToast(`Export failed: ${e}`); @@ -6900,6 +6912,30 @@ function AppShell({ > Export data… +
+ Scope:{" "} + + {onlyDeleted + ? "deleted only" + : showDeleted + ? "live + deleted" + : "live only"} + {" "} + — toggle Show deleted / Only deleted in the + Visibility panel to change. +

Layers