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
47
crates/archivr-server/static/assets/index-Bc8JhW_4.js
Normal file
47
crates/archivr-server/static/assets/index-Bc8JhW_4.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<script type="module" crossorigin src="/assets/index-mnEjEwmD.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bc8JhW_4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-sQJOY7Hx.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D8xkhocK.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export default function App() {
|
||||||
const [archives, setArchives] = useState([])
|
const [archives, setArchives] = useState([])
|
||||||
const [archiveId, setArchiveId] = useState(null)
|
const [archiveId, setArchiveId] = useState(null)
|
||||||
const [entries, setEntries] = useState([])
|
const [entries, setEntries] = useState([])
|
||||||
|
const [deletedUids, setDeletedUids] = useState(() => new Set())
|
||||||
const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry)
|
const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry)
|
||||||
const [selectedEntry, setSelectedEntry] = useState(null)
|
const [selectedEntry, setSelectedEntry] = useState(null)
|
||||||
const [selectedUids, setSelectedUids] = useState(() => {
|
const [selectedUids, setSelectedUids] = useState(() => {
|
||||||
|
|
@ -294,12 +295,20 @@ export default function App() {
|
||||||
if (next.size === 0) {
|
if (next.size === 0) {
|
||||||
selectEntry(null)
|
selectEntry(null)
|
||||||
} else if (next.size === 1) {
|
} 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 [remainingUid] = next
|
||||||
const cached = entryCacheRef.current.get(remainingUid)
|
const cached = entryCacheRef.current.get(remainingUid)
|
||||||
?? entries.find(x => x.entry_uid === remainingUid)
|
?? entries.find(x => x.entry_uid === remainingUid)
|
||||||
?? null
|
?? 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 {
|
} else {
|
||||||
selectEntry(null)
|
selectEntry(null)
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +317,7 @@ export default function App() {
|
||||||
setSelectedUids(new Set([entry.entry_uid]))
|
setSelectedUids(new Set([entry.entry_uid]))
|
||||||
selectEntry(entry)
|
selectEntry(entry)
|
||||||
}
|
}
|
||||||
}, [entries, selectedUids, selectEntry])
|
}, [entries, selectedUids, selectEntry, archiveId])
|
||||||
|
|
||||||
const handleTagFilterSet = useCallback((fullPath) => {
|
const handleTagFilterSet = useCallback((fullPath) => {
|
||||||
setTagFilter(fullPath)
|
setTagFilter(fullPath)
|
||||||
|
|
@ -360,18 +369,26 @@ export default function App() {
|
||||||
}, [archiveId, selectedEntry])
|
}, [archiveId, selectedEntry])
|
||||||
|
|
||||||
const handleEntryDeleted = useCallback((entryUid) => {
|
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))
|
setEntries(prev => prev.filter(e => e.entry_uid !== entryUid))
|
||||||
setSelectedEntry(prev => prev?.entry_uid === entryUid ? null : prev)
|
setSelectedEntry(prev => prev?.entry_uid === entryUid ? null : prev)
|
||||||
setSelectedEntryUid(prev => prev === entryUid ? null : prev)
|
setSelectedEntryUid(prev => prev === entryUid ? null : prev)
|
||||||
setSelectedUids(prev => { const n = new Set(prev); n.delete(entryUid); return n })
|
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 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)))
|
setEntries(prev => prev.filter(e => !uids.has(e.entry_uid)))
|
||||||
setSelectedUids(new Set())
|
setSelectedUids(new Set())
|
||||||
setSelectedEntry(null)
|
setSelectedEntry(null)
|
||||||
setSelectedEntryUid(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
|
// Auto-snap: drive selectedEntryUid from selectedUids so URL sync and detail
|
||||||
// panel stay correct. size >= 2 clears single-entry state (bulk panel takes over).
|
// panel stay correct. size >= 2 clears single-entry state (bulk panel takes over).
|
||||||
|
|
@ -581,6 +598,7 @@ export default function App() {
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
archiveId={archiveId}
|
archiveId={archiveId}
|
||||||
pendingCaptures={pendingCaptures}
|
pendingCaptures={pendingCaptures}
|
||||||
|
deletedUids={deletedUids}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === 'runs' && <RunsView runs={runs} />}
|
{view === 'runs' && <RunsView runs={runs} />}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import SkeletonEntryRow from './SkeletonEntryRow';
|
||||||
|
|
||||||
import EntryRow from './EntryRow';
|
import EntryRow from './EntryRow';
|
||||||
|
|
||||||
export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [] }) {
|
export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [], deletedUids }) {
|
||||||
return (
|
return (
|
||||||
<section id="archive-view" className="view is-active">
|
<section id="archive-view" className="view is-active">
|
||||||
<div className="entry-table">
|
<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)}
|
isMultiSelected={selectedUids.size >= 2 && selectedUids.has(entry.entry_uid)}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
selectedUids={selectedUids}
|
selectedUids={selectedUids}
|
||||||
|
deletedUids={deletedUids}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ import { useState } from 'react';
|
||||||
import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils';
|
import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils';
|
||||||
import { fetchEntryChildren } from '../api';
|
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 isSelected = (selectedUids?.size === 1) && selectedUids.has(entry.entry_uid);
|
||||||
const isMultiSelected = (selectedUids?.size >= 2) && selectedUids.has(entry.entry_uid);
|
const isMultiSelected = (selectedUids?.size >= 2) && selectedUids.has(entry.entry_uid);
|
||||||
|
|
||||||
const cls = ['child-entry-row',
|
const cls = ['child-entry-row',
|
||||||
|
index % 2 === 0 ? 'child-entry-row--light' : 'child-entry-row--dark',
|
||||||
isSelected && 'is-selected',
|
isSelected && 'is-selected',
|
||||||
isMultiSelected && 'is-multi-selected',
|
isMultiSelected && 'is-multi-selected',
|
||||||
].filter(Boolean).join(' ');
|
].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 [favFailed, setFavFailed] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [children, setChildren] = useState(null);
|
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 className="url-cell col-url">{valueText(entry.original_url)}</div>
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="child-entries" aria-label={`${entry.child_count} child entries`}>
|
<>
|
||||||
{childrenLoading && <div className="child-entries-loading">Loading…</div>}
|
{childrenLoading && <div className="child-entries-loading">Loading…</div>}
|
||||||
{children && children.map(child => (
|
<div className="child-entries" aria-label={`${entry.child_count} child entries`}>
|
||||||
<ChildRow
|
{children && children
|
||||||
key={child.entry_uid}
|
.filter(c => !deletedUids?.has(c.entry_uid))
|
||||||
entry={child}
|
.map((child, idx) => (
|
||||||
onRowClick={onRowClick}
|
<ChildRow
|
||||||
selectedUids={selectedUids}
|
key={child.entry_uid}
|
||||||
/>
|
entry={child}
|
||||||
))}
|
index={idx}
|
||||||
</div>
|
onRowClick={onRowClick}
|
||||||
|
selectedUids={selectedUids}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2796,8 +2796,8 @@ body.has-audio-bar { padding-bottom: 56px; }
|
||||||
}
|
}
|
||||||
.child-entry-row:hover { opacity: 1; }
|
.child-entry-row:hover { opacity: 1; }
|
||||||
.child-entry-row:last-child { border-bottom: none; }
|
.child-entry-row:last-child { border-bottom: none; }
|
||||||
.child-entry-row:nth-child(odd) { background: var(--paper-3); }
|
.child-entry-row--light { background: var(--paper-3); }
|
||||||
.child-entry-row:nth-child(even) { background: #f2ede5; }
|
.child-entry-row--dark { background: #f2ede5; }
|
||||||
.child-entry-row.is-selected {
|
.child-entry-row.is-selected {
|
||||||
background: #eee2d2;
|
background: #eee2d2;
|
||||||
box-shadow: inset 0 0 0 2px var(--accent);
|
box-shadow: inset 0 0 0 2px var(--accent);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue