deleted export
This commit is contained in:
@@ -2008,15 +2008,33 @@ 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<bool>,
|
||||
#[allow(non_snake_case)] only_deleted: Option<bool>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<usize, String> {
|
||||
let assets: Vec<Asset> = 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",
|
||||
)
|
||||
FROM assets {} ORDER BY asset_type, asset_name, id",
|
||||
where_clause
|
||||
);
|
||||
let assets: Vec<Asset> = sqlx::query_as(&sql)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2026,8 +2044,8 @@ pub async fn export_assets(
|
||||
// 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<i64, String> = 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<serde_json::Value> = vec![];
|
||||
let mut range_arr: Vec<serde_json::Value> = vec![];
|
||||
let mut deleted_fixed: Vec<serde_json::Value> = vec![];
|
||||
let mut deleted_range: Vec<serde_json::Value> = vec![];
|
||||
for a in &assets {
|
||||
if a.asset_type == "fixed" {
|
||||
if let (Some(la), Some(ln)) = (a.lat, a.lng) {
|
||||
@@ -2235,8 +2261,12 @@ pub async fn export_assets(
|
||||
inject_bbox(&mut obj, "", bb);
|
||||
}
|
||||
}
|
||||
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)) =
|
||||
(a.lat, a.lng, a.start_lat, a.start_lng, a.end_lat, a.end_lng)
|
||||
@@ -2271,14 +2301,24 @@ pub async fn export_assets(
|
||||
inject_bbox(&mut obj, "end_", bb);
|
||||
}
|
||||
}
|
||||
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,
|
||||
}))
|
||||
}
|
||||
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" => {
|
||||
|
||||
40
src/App.tsx
40
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<number>("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…
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
fontSize: 10,
|
||||
color: onlyDeleted
|
||||
? "#c0392b"
|
||||
: showDeleted
|
||||
? "#d35400"
|
||||
: "#7f8c8d",
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
title="Export scope follows the Visibility filters on the left panel"
|
||||
>
|
||||
Scope:{" "}
|
||||
<strong>
|
||||
{onlyDeleted
|
||||
? "deleted only"
|
||||
: showDeleted
|
||||
? "live + deleted"
|
||||
: "live only"}
|
||||
</strong>{" "}
|
||||
— toggle <em>Show deleted</em> / <em>Only deleted</em> in the
|
||||
Visibility panel to change.
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ marginTop: 16 }}>Layers</h3>
|
||||
<div className="layer-toggles">
|
||||
|
||||
Reference in New Issue
Block a user