feat: Tags entry in the Library sidebar
Adds an expandable Tags node alongside All Photos / Rated /
Duplicates / Discarded. The children are populated dynamically from
useTagsQuery — one row per tag, showing the tag name and its photo
count badge. Click a tag row to filter the timeline to just that
tag (single-tag), with the active highlight following the filter
store.
Multi-tag filtering still lives in the FilterBar; the sidebar entry
is the quick "show me everything in this tag" affordance.
Implementation
- New 'tags' library tree node with children: allTags.map(...)
- 'tag-{id}' click handler in applyLibraryNode → clearAll() +
setTagIds([id])
- isItemActive recognises a tag row as selected only when the
filter store has exactly that single tag id, so combining it with
multi-tag filter mode in the FilterBar doesn't leave a stale
highlight.
- Tags section is collapsed by default like other library nodes; no
effect when there are no tags yet (children list is empty).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
HardDrive,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Tag as TagIcon,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { sourceFolders, library, photos as photosApi, type FolderTreeNode } from '../../services/api'
|
||||
@@ -19,6 +20,7 @@ import { useFilterStore } from '../../store/filterStore'
|
||||
import { HeapsPanel } from '../heaps/HeapsPanel'
|
||||
import { PHOTO_DRAG_MIME } from '../timeline/PhotoThumbnail'
|
||||
import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery'
|
||||
import { useTagsQuery } from '../../hooks/useTagsQuery'
|
||||
|
||||
interface TreeItem {
|
||||
id: string
|
||||
@@ -44,8 +46,11 @@ export function LeftSidebar() {
|
||||
const setFlag = useFilterStore((s) => s.setFlag)
|
||||
const setFolderId = useFilterStore((s) => s.setFolderId)
|
||||
const setDuplicates = useFilterStore((s) => s.setDuplicates)
|
||||
const setTagIds = useFilterStore((s) => s.setTagIds)
|
||||
const filterFolderId = useFilterStore((s) => s.folderId)
|
||||
const filterDuplicates = useFilterStore((s) => s.duplicates)
|
||||
const filterTagIds = useFilterStore((s) => s.tagIds)
|
||||
const { data: allTags = [] } = useTagsQuery()
|
||||
const [dropTargetId, setDropTargetId] = useState<string | null>(null)
|
||||
|
||||
// Bulk discard mutation for the drag-onto-Discarded interaction.
|
||||
@@ -138,6 +143,14 @@ export function LeftSidebar() {
|
||||
setDuplicates(true)
|
||||
break
|
||||
default:
|
||||
if (id.startsWith('tag-')) {
|
||||
// Tag rows: filter to that single tag. Multi-tag filtering is
|
||||
// available via the FilterBar.
|
||||
const tagId = id.slice('tag-'.length)
|
||||
clearAllFilters()
|
||||
setTagIds([tagId])
|
||||
return
|
||||
}
|
||||
if (id.startsWith('folder-')) {
|
||||
// Folder rows: filter to that folder, clear other filters that
|
||||
// would compete (heap, discarded, etc.) so the user sees what they
|
||||
@@ -219,6 +232,17 @@ export function LeftSidebar() {
|
||||
{ id: 'rated', label: 'Rated', icon: <Star className="h-4 w-4" />, count: 0 },
|
||||
{ id: 'duplicates', label: 'Duplicates', icon: <Copy className="h-4 w-4" />, count: 0 },
|
||||
{ id: 'discarded', label: 'Discarded', icon: <Trash2 className="h-4 w-4" />, count: 0 },
|
||||
{
|
||||
id: 'tags',
|
||||
label: 'Tags',
|
||||
icon: <TagIcon className="h-4 w-4" />,
|
||||
children: allTags.map((tag) => ({
|
||||
id: `tag-${tag.id}`,
|
||||
label: tag.name,
|
||||
icon: <TagIcon className="h-4 w-4" />,
|
||||
count: tag.photo_count,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -237,6 +261,10 @@ export function LeftSidebar() {
|
||||
if (id.startsWith('folder-')) {
|
||||
return filterFolderId === id.slice('folder-'.length)
|
||||
}
|
||||
if (id.startsWith('tag-')) {
|
||||
const tagId = id.slice('tag-'.length)
|
||||
return filterTagIds.length === 1 && filterTagIds[0] === tagId
|
||||
}
|
||||
if (id === 'all-photos') {
|
||||
return filterFolderId === null && selectedItem === 'all-photos'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user