mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
fix(core+frontend): child entry selection and delete correctness
database.rs — delete_entry: - subtree_ids now uses WHERE id = ?1 OR root_entry_id = ?1 so the entry itself is always included; previously a child deletion passed an empty vec to cascade_cached_bytes_after_subtree_delete (no grandchildren exist), leaving cached_bytes stale on entries sharing that child's blobs App.jsx — handleRowClick: - Shift-range now queries DOM order (#entries-body [data-entry-uid]) instead of entries.findIndex(); child rows are in the DOM but not in the root entries array, so findIndex always returned -1 for them - Ctrl/meta branch computes next set before the state update so selectEntry can fire synchronously for child rows; auto-snap only restores root entries, so ctrl-clicking a lone child never loaded its detail panel — now calls selectEntry(entry) when the child ends up as the sole selection, selectEntry(null) on multi or deselect - selectedUids added to handleRowClick's useCallback dep array
This commit is contained in:
parent
d07e4d7ac9
commit
03406099de
4 changed files with 34 additions and 25 deletions
|
|
@ -1627,9 +1627,14 @@ pub fn delete_entry(conn: &Connection, entry_uid: &str) -> Result<bool> {
|
|||
None => return Ok(false),
|
||||
};
|
||||
|
||||
// Collect the full subtree while rows still exist.
|
||||
// Collect the full subtree (entry itself + any descendants) while rows still exist.
|
||||
// Must include the entry itself: for a child entry root_entry_id = ?1 returns nothing
|
||||
// (no grandchildren), so without `id = ?1` the set would be empty and
|
||||
// cascade_cached_bytes_after_subtree_delete would not recalculate shared-blob totals.
|
||||
let subtree_ids: Vec<i64> = {
|
||||
let mut stmt = conn.prepare("SELECT id FROM archived_entries WHERE root_entry_id = ?1")?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id FROM archived_entries WHERE id = ?1 OR root_entry_id = ?1",
|
||||
)?;
|
||||
stmt.query_map([entry_id], |row| row.get(0))?
|
||||
.collect::<rusqlite::Result<_>>()?
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Archivr</title>
|
||||
<script type="module" crossorigin src="/assets/index-B06KmLG0.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DQR_MNv8.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-sQJOY7Hx.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -262,10 +262,13 @@ export default function App() {
|
|||
const handleRowClick = useCallback((entry, e) => {
|
||||
if (e.shiftKey && lastAnchorIndexRef.current !== null) {
|
||||
e.preventDefault()
|
||||
const anchorIdx = entries.findIndex(x => x.entry_uid === lastAnchorIndexRef.current)
|
||||
const clickIdx = entries.findIndex(x => x.entry_uid === entry.entry_uid)
|
||||
// Use DOM order so child rows (not in the `entries` array) participate
|
||||
// in range selection. Every rendered row has data-entry-uid.
|
||||
const allNodes = [...document.querySelectorAll('#entries-body [data-entry-uid]')]
|
||||
const anchorIdx = allNodes.findIndex(n => n.dataset.entryUid === lastAnchorIndexRef.current)
|
||||
const clickIdx = allNodes.findIndex(n => n.dataset.entryUid === entry.entry_uid)
|
||||
if (anchorIdx === -1 || clickIdx === -1) {
|
||||
// anchor evicted by search/filter/delete — fall back to single select
|
||||
// anchor no longer visible — fall back to single select
|
||||
lastAnchorIndexRef.current = entry.entry_uid
|
||||
setSelectedUids(new Set([entry.entry_uid]))
|
||||
selectEntry(entry)
|
||||
|
|
@ -273,24 +276,25 @@ export default function App() {
|
|||
}
|
||||
const lo = Math.min(anchorIdx, clickIdx)
|
||||
const hi = Math.max(anchorIdx, clickIdx)
|
||||
const range = entries.slice(lo, hi + 1)
|
||||
const uids = new Set(range.map(x => x.entry_uid))
|
||||
const uids = new Set(allNodes.slice(lo, hi + 1).map(n => n.dataset.entryUid))
|
||||
setSelectedUids(uids)
|
||||
if (uids.size === 1) selectEntry(range[0])
|
||||
if (uids.size === 1) selectEntry(entry)
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
lastAnchorIndexRef.current = entry.entry_uid
|
||||
setSelectedUids(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(entry.entry_uid)) next.delete(entry.entry_uid)
|
||||
else next.add(entry.entry_uid)
|
||||
return next
|
||||
})
|
||||
const next = new Set(selectedUids)
|
||||
if (next.has(entry.entry_uid)) next.delete(entry.entry_uid)
|
||||
else next.add(entry.entry_uid)
|
||||
setSelectedUids(next)
|
||||
// Auto-snap only restores root entries; explicitly drive selectEntry so
|
||||
// ctrl-clicking a single child row loads its detail and clears on multi.
|
||||
if (next.size === 1 && next.has(entry.entry_uid)) selectEntry(entry)
|
||||
else if (next.size !== 1) selectEntry(null)
|
||||
} else {
|
||||
lastAnchorIndexRef.current = entry.entry_uid
|
||||
setSelectedUids(new Set([entry.entry_uid]))
|
||||
selectEntry(entry)
|
||||
}
|
||||
}, [entries, selectEntry])
|
||||
}, [entries, selectedUids, selectEntry])
|
||||
|
||||
const handleTagFilterSet = useCallback((fullPath) => {
|
||||
setTagFilter(fullPath)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue