1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat: implement entry deletion

- database: cascade_cached_bytes_after_subtree_delete — one-pass set-aware
  SQL that excludes the entire subtree simultaneously, avoiding sibling-blob
  cross-counting bug
- database: delete_entry — collects subtree IDs, runs set-aware cascade,
  NULLs archive_run_items.produced_entry_id (FK blocker), deletes children
  then root; ON DELETE CASCADE handles artifacts/tags/collections
- server: DELETE /api/archives/:archive_id/entries/:entry_uid route,
  wrapped in a transaction for atomicity
- frontend: deleteEntry API call, handleEntryDeleted callback in App.jsx
  (optimistic list removal + selection clear), Delete entry button in
  ContextRail with confirm guard
- tests: 4 database tests (unknown uid, subtree removal, run_item nulling,
  cached_bytes recalculation) + 3 route tests (204+gone, 404, 401)
This commit is contained in:
TheGeneralist 2026-07-04 13:46:19 +02:00
parent cad3ae9885
commit ed1f883ff1
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
15 changed files with 416 additions and 128 deletions

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections, updateEntryTitle } from '../api'
import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections, updateEntryTitle, deleteEntry } from '../api'
import { formatTimestamp, formatBytes, valueText, sourceIconSvg, displayPath } from '../utils'
const VIS_LABEL = { 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }
@ -11,7 +11,7 @@ const ExternalIcon = () => (
</svg>
)
export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, tagNodes, onTagsRefresh, onEntryTitleChange, humanizeTags }) {
export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, tagNodes, onTagsRefresh, onEntryTitleChange, onEntryDeleted, humanizeTags }) {
const [detail, setDetail] = useState(null)
const [tags, setTags] = useState([])
const [assignInput, setAssignInput] = useState('')
@ -86,6 +86,17 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
}
}
async function handleDeleteEntry() {
if (!selectedEntry || !archiveId) return
if (!window.confirm('Delete this entry? This cannot be undone.')) return
try {
await deleteEntry(archiveId, selectedEntry.entry_uid)
onEntryDeleted?.(selectedEntry.entry_uid)
} catch {
// silently ignore entry stays selected if delete failed
}
}
const metaRows = detail ? [
['Added', formatTimestamp(detail.summary.archived_at)],
['Source', detail.summary.source_kind],
@ -232,6 +243,12 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
))}
</div>
)}
<div className="rail-delete-zone">
<button className="rail-delete-btn" onClick={handleDeleteEntry}>
Delete entry
</button>
</div>
</>
)}
</aside>