mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +02:00
fix(frontend): child row stripes, deleted-child visibility, selection completeness
EntryRow.jsx / ChildRow: - Index-based light/dark classes (child-entry-row--light/dark) replace nth-child rules; parity comes from children.map idx so no sibling — including the loading div — can shift the stripe order - Loading div moved outside .child-entries so it never affects child row ordering at all - Accept deletedUids prop; filter expanded children array before render so deleted children disappear immediately without waiting for reload EntriesView.jsx: thread deletedUids through to EntryRow App.jsx: - Add deletedUids state; handleEntryDeleted/handleBulkDeleted populate it - isRoot/hasChildDelete computed from entries before setEntries (safe in StrictMode — no side-effects inside updater functions) - Child delete triggers loadEntries to refresh stale parent child_count and total_artifact_bytes - handleRowClick ctrl/meta cache-miss branch uses det.summary (not det) from fetchEntryDetail; archiveId added to dep array - handleRowClick dep array includes archiveId
This commit is contained in:
parent
74d3b0b3be
commit
5f575ac8d7
8 changed files with 95 additions and 70 deletions
|
|
@ -86,6 +86,7 @@ export default function App() {
|
|||
const [archives, setArchives] = useState([])
|
||||
const [archiveId, setArchiveId] = useState(null)
|
||||
const [entries, setEntries] = useState([])
|
||||
const [deletedUids, setDeletedUids] = useState(() => new Set())
|
||||
const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry)
|
||||
const [selectedEntry, setSelectedEntry] = useState(null)
|
||||
const [selectedUids, setSelectedUids] = useState(() => {
|
||||
|
|
@ -294,12 +295,20 @@ export default function App() {
|
|||
if (next.size === 0) {
|
||||
selectEntry(null)
|
||||
} else if (next.size === 1) {
|
||||
// Resolve the remaining UID — may be a child not in root entries array.
|
||||
// Resolve the remaining UID — may be a child not in the root entries array
|
||||
// and not yet cached (e.g. picked up via shift-range without a direct click).
|
||||
const [remainingUid] = next
|
||||
const cached = entryCacheRef.current.get(remainingUid)
|
||||
?? entries.find(x => x.entry_uid === remainingUid)
|
||||
?? null
|
||||
selectEntry(cached)
|
||||
if (cached) {
|
||||
selectEntry(cached)
|
||||
} else {
|
||||
// Cache miss — fetch from server rather than clearing the detail panel.
|
||||
fetchEntryDetail(archiveId, remainingUid)
|
||||
.then(det => { if (det?.summary) selectEntry(det.summary) })
|
||||
.catch(() => {})
|
||||
}
|
||||
} else {
|
||||
selectEntry(null)
|
||||
}
|
||||
|
|
@ -308,7 +317,7 @@ export default function App() {
|
|||
setSelectedUids(new Set([entry.entry_uid]))
|
||||
selectEntry(entry)
|
||||
}
|
||||
}, [entries, selectedUids, selectEntry])
|
||||
}, [entries, selectedUids, selectEntry, archiveId])
|
||||
|
||||
const handleTagFilterSet = useCallback((fullPath) => {
|
||||
setTagFilter(fullPath)
|
||||
|
|
@ -360,18 +369,26 @@ export default function App() {
|
|||
}, [archiveId, selectedEntry])
|
||||
|
||||
const handleEntryDeleted = useCallback((entryUid) => {
|
||||
const isRoot = entries.some(e => e.entry_uid === entryUid)
|
||||
setDeletedUids(prev => { const n = new Set(prev); n.add(entryUid); return n })
|
||||
setEntries(prev => prev.filter(e => e.entry_uid !== entryUid))
|
||||
setSelectedEntry(prev => prev?.entry_uid === entryUid ? null : prev)
|
||||
setSelectedEntryUid(prev => prev === entryUid ? null : prev)
|
||||
setSelectedUids(prev => { const n = new Set(prev); n.delete(entryUid); return n })
|
||||
}, [])
|
||||
// Child delete: parent row's child_count/size are stale — reload after state updates.
|
||||
if (!isRoot) loadEntries(archiveId, searchQuery, tagFilter)
|
||||
}, [entries, archiveId, searchQuery, tagFilter, loadEntries])
|
||||
|
||||
const handleBulkDeleted = useCallback((uids) => {
|
||||
const rootUids = new Set(entries.map(e => e.entry_uid))
|
||||
const hasChildDelete = [...uids].some(u => !rootUids.has(u))
|
||||
setDeletedUids(prev => { const n = new Set(prev); uids.forEach(u => n.add(u)); return n })
|
||||
setEntries(prev => prev.filter(e => !uids.has(e.entry_uid)))
|
||||
setSelectedUids(new Set())
|
||||
setSelectedEntry(null)
|
||||
setSelectedEntryUid(null)
|
||||
}, [])
|
||||
if (hasChildDelete) loadEntries(archiveId, searchQuery, tagFilter)
|
||||
}, [entries, archiveId, searchQuery, tagFilter, loadEntries])
|
||||
|
||||
// Auto-snap: drive selectedEntryUid from selectedUids so URL sync and detail
|
||||
// panel stay correct. size >= 2 clears single-entry state (bulk panel takes over).
|
||||
|
|
@ -581,6 +598,7 @@ export default function App() {
|
|||
onRowClick={handleRowClick}
|
||||
archiveId={archiveId}
|
||||
pendingCaptures={pendingCaptures}
|
||||
deletedUids={deletedUids}
|
||||
/>
|
||||
)}
|
||||
{view === 'runs' && <RunsView runs={runs} />}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import SkeletonEntryRow from './SkeletonEntryRow';
|
|||
|
||||
import EntryRow from './EntryRow';
|
||||
|
||||
export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [] }) {
|
||||
export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [], deletedUids }) {
|
||||
return (
|
||||
<section id="archive-view" className="view is-active">
|
||||
<div className="entry-table">
|
||||
|
|
@ -27,6 +27,7 @@ export default function EntriesView({ entries, selectedUids, onRowClick, archive
|
|||
isMultiSelected={selectedUids.size >= 2 && selectedUids.has(entry.entry_uid)}
|
||||
onRowClick={onRowClick}
|
||||
selectedUids={selectedUids}
|
||||
deletedUids={deletedUids}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import { useState } from 'react';
|
|||
import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils';
|
||||
import { fetchEntryChildren } from '../api';
|
||||
|
||||
function ChildRow({ entry, onRowClick, selectedUids }) {
|
||||
function ChildRow({ entry, index, onRowClick, selectedUids }) {
|
||||
const isSelected = (selectedUids?.size === 1) && selectedUids.has(entry.entry_uid);
|
||||
const isMultiSelected = (selectedUids?.size >= 2) && selectedUids.has(entry.entry_uid);
|
||||
|
||||
const cls = ['child-entry-row',
|
||||
index % 2 === 0 ? 'child-entry-row--light' : 'child-entry-row--dark',
|
||||
isSelected && 'is-selected',
|
||||
isMultiSelected && 'is-multi-selected',
|
||||
].filter(Boolean).join(' ');
|
||||
|
|
@ -39,7 +40,7 @@ function ChildRow({ entry, onRowClick, selectedUids }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function EntryRow({ entry, archiveId, isSelected, isMultiSelected, onRowClick, selectedUids }) {
|
||||
export default function EntryRow({ entry, archiveId, isSelected, isMultiSelected, onRowClick, selectedUids, deletedUids }) {
|
||||
const [favFailed, setFavFailed] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [children, setChildren] = useState(null);
|
||||
|
|
@ -150,17 +151,22 @@ export default function EntryRow({ entry, archiveId, isSelected, isMultiSelected
|
|||
<div className="url-cell col-url">{valueText(entry.original_url)}</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="child-entries" aria-label={`${entry.child_count} child entries`}>
|
||||
<>
|
||||
{childrenLoading && <div className="child-entries-loading">Loading…</div>}
|
||||
{children && children.map(child => (
|
||||
<ChildRow
|
||||
key={child.entry_uid}
|
||||
entry={child}
|
||||
onRowClick={onRowClick}
|
||||
selectedUids={selectedUids}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="child-entries" aria-label={`${entry.child_count} child entries`}>
|
||||
{children && children
|
||||
.filter(c => !deletedUids?.has(c.entry_uid))
|
||||
.map((child, idx) => (
|
||||
<ChildRow
|
||||
key={child.entry_uid}
|
||||
entry={child}
|
||||
index={idx}
|
||||
onRowClick={onRowClick}
|
||||
selectedUids={selectedUids}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2796,8 +2796,8 @@ body.has-audio-bar { padding-bottom: 56px; }
|
|||
}
|
||||
.child-entry-row:hover { opacity: 1; }
|
||||
.child-entry-row:last-child { border-bottom: none; }
|
||||
.child-entry-row:nth-child(odd) { background: var(--paper-3); }
|
||||
.child-entry-row:nth-child(even) { background: #f2ede5; }
|
||||
.child-entry-row--light { background: var(--paper-3); }
|
||||
.child-entry-row--dark { background: #f2ede5; }
|
||||
.child-entry-row.is-selected {
|
||||
background: #eee2d2;
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue