F14: add per-activity delete (DELETE /api/activity/{id} + drawer button)
Server endpoint removes the activity JSON, GeoJSON, timeseries, sidecar edit, and images directory. Also purges the dedup cache entry so the file can be re-uploaded if needed. Runs merge_all + rebuild afterwards. EditDrawer: two-click delete button (click once → "Confirm delete?", click again → deletes). On success, dispatches 'deleted' event. ActivityDetail navigates back to the feed on delete.
This commit is contained in:
@@ -597,6 +597,56 @@ async def post_activity(
|
|||||||
return JSONResponse({"ok": True})
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/activity/{activity_id}")
|
||||||
|
async def delete_activity(
|
||||||
|
activity_id: str,
|
||||||
|
bincio_session: Optional[str] = Cookie(default=None),
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Delete a single activity and all associated files for the logged-in user."""
|
||||||
|
user = _require_user(bincio_session)
|
||||||
|
_check_id(activity_id)
|
||||||
|
dd = _get_data_dir() / user.handle
|
||||||
|
acts_dir = dd / "activities"
|
||||||
|
|
||||||
|
json_path = acts_dir / f"{activity_id}.json"
|
||||||
|
if not json_path.exists():
|
||||||
|
raise HTTPException(404, "Activity not found")
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Remove the source files (activities dir)
|
||||||
|
for suffix in (".json", ".geojson", ".timeseries.json"):
|
||||||
|
p = acts_dir / f"{activity_id}{suffix}"
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
# Remove sidecar edit and images
|
||||||
|
sidecar = dd / "edits" / f"{activity_id}.md"
|
||||||
|
sidecar.unlink(missing_ok=True)
|
||||||
|
images_dir = dd / "edits" / "images" / activity_id
|
||||||
|
if images_dir.exists():
|
||||||
|
shutil.rmtree(images_dir)
|
||||||
|
|
||||||
|
# Remove from dedup cache so the file can be re-uploaded if needed
|
||||||
|
cache_path = dd / ".bincio_cache.json"
|
||||||
|
if cache_path.exists():
|
||||||
|
try:
|
||||||
|
cache = json.loads(cache_path.read_text(encoding="utf-8"))
|
||||||
|
if isinstance(cache, dict) and "activities" in cache:
|
||||||
|
cache["activities"] = [
|
||||||
|
a for a in cache["activities"] if a.get("id") != activity_id
|
||||||
|
]
|
||||||
|
cache_path.write_text(json.dumps(cache, indent=2, ensure_ascii=False))
|
||||||
|
except Exception:
|
||||||
|
pass # corrupt cache — leave it; next extract will rebuild
|
||||||
|
|
||||||
|
# Full merge needed: activity removed from index
|
||||||
|
from bincio.render.merge import merge_all
|
||||||
|
merge_all(dd)
|
||||||
|
_trigger_rebuild(user.handle)
|
||||||
|
|
||||||
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/activity/{activity_id}/images")
|
@app.get("/api/activity/{activity_id}/images")
|
||||||
async def list_images(
|
async def list_images(
|
||||||
activity_id: str,
|
activity_id: str,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<svelte:window on:keydown={onKeydown} />
|
<svelte:window on:keydown={onKeydown} />
|
||||||
|
|
||||||
{#if editOpen && editEnabled}
|
{#if editOpen && editEnabled}
|
||||||
<EditDrawer activityId={activity.id} {editUrl} on:saved={onSaved} on:close={() => editOpen = false} />
|
<EditDrawer activityId={activity.id} {editUrl} on:saved={onSaved} on:close={() => editOpen = false} on:deleted={() => { window.location.href = base; }} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Lightbox -->
|
<!-- Lightbox -->
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
export let activityId: string;
|
export let activityId: string;
|
||||||
export let editUrl: string;
|
export let editUrl: string;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ saved: { title: string; description: string }; close: void }>();
|
const dispatch = createEventDispatcher<{ saved: { title: string; description: string }; close: void; deleted: void }>();
|
||||||
|
|
||||||
const SPORTS: Sport[] = ['cycling', 'running', 'hiking', 'walking', 'swimming', 'skiing', 'other'];
|
const SPORTS: Sport[] = ['cycling', 'running', 'hiking', 'walking', 'swimming', 'skiing', 'other'];
|
||||||
const STAT_PANELS = [
|
const STAT_PANELS = [
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
let saving = false;
|
let saving = false;
|
||||||
let saveStatus = '';
|
let saveStatus = '';
|
||||||
let saveOk = false;
|
let saveOk = false;
|
||||||
|
let confirmDelete = false;
|
||||||
|
let deleting = false;
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
let title = '';
|
let title = '';
|
||||||
@@ -120,6 +122,22 @@
|
|||||||
: [...hideStats, key];
|
: [...hideStats, key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteActivity() {
|
||||||
|
if (!confirmDelete) { confirmDelete = true; return; }
|
||||||
|
deleting = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(api, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
dispatch('deleted');
|
||||||
|
} catch (e: any) {
|
||||||
|
saveStatus = `Delete failed: ${e.message}`;
|
||||||
|
saveOk = false;
|
||||||
|
confirmDelete = false;
|
||||||
|
} finally {
|
||||||
|
deleting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -284,11 +302,26 @@
|
|||||||
<div class="px-5 py-4 border-t border-zinc-800 flex items-center gap-3 shrink-0">
|
<div class="px-5 py-4 border-t border-zinc-800 flex items-center gap-3 shrink-0">
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-sm font-medium rounded-lg transition-colors"
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-sm font-medium rounded-lg transition-colors"
|
||||||
disabled={saving}
|
disabled={saving || deleting}
|
||||||
on:click={save}
|
on:click={save}
|
||||||
>
|
>
|
||||||
{saving ? 'Saving…' : 'Save'}
|
{saving ? 'Saving…' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 text-sm font-medium rounded-lg border transition-colors disabled:opacity-40 ml-auto"
|
||||||
|
class:border-zinc-700={!confirmDelete}
|
||||||
|
class:text-zinc-500={!confirmDelete}
|
||||||
|
class:hover:border-red-600={!confirmDelete}
|
||||||
|
class:hover:text-red-400={!confirmDelete}
|
||||||
|
class:border-red-500={confirmDelete}
|
||||||
|
class:text-red-400={confirmDelete}
|
||||||
|
class:bg-red-950={confirmDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
on:click={deleteActivity}
|
||||||
|
on:blur={() => confirmDelete = false}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting…' : confirmDelete ? 'Confirm delete?' : 'Delete'}
|
||||||
|
</button>
|
||||||
{#if saveStatus}
|
{#if saveStatus}
|
||||||
<span class="text-xs" class:text-green-400={saveOk} class:text-red-400={!saveOk}>
|
<span class="text-xs" class:text-green-400={saveOk} class:text-red-400={!saveOk}>
|
||||||
{saveStatus}
|
{saveStatus}
|
||||||
|
|||||||
Reference in New Issue
Block a user