feat: discard view with restore and empty actions
Adds the destructive-action loop the discard concept needed:
- Click "Discarded" in the left sidebar → activates the discarded
filter; the timeline reloads showing discarded photos.
- DiscardActionBar appears at the top of the timeline only when the
discarded filter is active. Shows the count, a Restore button (when
photos are selected), and an Empty discard pile button.
- Empty action goes through a ConfirmDialog (new tiny reusable modal,
same overlay pattern as AddSourceFolderDialog).
- Restore goes through POST /api/v1/discard/restore.
- DELETE /api/v1/discard/empty now actually os.unlink()s the files
from disk in addition to removing the DB rows. Per-file failures
are logged and reported in the response so a single permission
error doesn't abort the batch.
Other library nodes wired in passing:
- "All Photos" → clearAll()
- "Rated" → setRatingMin(1)
- "Flagged" → setFlag('picked')
- "Discarded" → setFlag('discarded')
- "By Date" left unwired (needs a date-grouping UI)
Single-photo restore via the U keyboard shortcut already worked from
an earlier round, no change needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import { AddSourceFolderDialog } from '../dialogs/AddSourceFolderDialog'
|
||||
import { sourceFolders, library } from '../../services/api'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from '../ToastContainer'
|
||||
import { useFilterStore } from '../../store/filterStore'
|
||||
|
||||
interface TreeItem {
|
||||
id: string
|
||||
@@ -33,8 +34,34 @@ export function LeftSidebar() {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>('all-photos')
|
||||
const [showAddFolderDialog, setShowAddFolderDialog] = useState(false)
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const clearAllFilters = useFilterStore((s) => s.clearAll)
|
||||
const setRatingMin = useFilterStore((s) => s.setRatingMin)
|
||||
const setFlag = useFilterStore((s) => s.setFlag)
|
||||
|
||||
// Map a library tree id to a filter-store mutation. Each "virtual node" in
|
||||
// the library tree is just a saved filter preset.
|
||||
const applyLibraryNode = (id: string) => {
|
||||
switch (id) {
|
||||
case 'all-photos':
|
||||
clearAllFilters()
|
||||
break
|
||||
case 'rated':
|
||||
clearAllFilters()
|
||||
setRatingMin(1)
|
||||
break
|
||||
case 'flagged':
|
||||
clearAllFilters()
|
||||
setFlag('picked')
|
||||
break
|
||||
case 'discarded':
|
||||
clearAllFilters()
|
||||
setFlag('discarded')
|
||||
break
|
||||
// 'by-date' is purely visual until we add a date-grouping UI
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch folders from API
|
||||
const { data: foldersData, refetch: refetchFolders } = useQuery({
|
||||
@@ -152,6 +179,8 @@ export function LeftSidebar() {
|
||||
setSelectedItem(item.id)
|
||||
if (hasChildren) {
|
||||
toggleExpanded(item.id)
|
||||
} else {
|
||||
applyLibraryNode(item.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user