diff --git a/frontend/src/pages/Process.tsx b/frontend/src/pages/Process.tsx index 33604ef..d84ffe8 100644 --- a/frontend/src/pages/Process.tsx +++ b/frontend/src/pages/Process.tsx @@ -235,7 +235,7 @@ export default function Process() {
@@ -271,11 +271,10 @@ export default function Process() { e.preventDefault() } }} - placeholder="All unprocessed photos" - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + className="w-32 px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" disabled={isProcessing} /> -

+

Leave empty to process all unprocessed photos

@@ -342,7 +341,7 @@ export default function Process() { type="button" onClick={handleStartProcessing} disabled={isProcessing} - className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" + className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" > {isProcessing ? 'Processing...' : 'Start Processing'} diff --git a/frontend/src/pages/Scan.tsx b/frontend/src/pages/Scan.tsx index 71d1856..9b15e59 100644 --- a/frontend/src/pages/Scan.tsx +++ b/frontend/src/pages/Scan.tsx @@ -289,9 +289,9 @@ export default function Scan() { type="button" onClick={handleScanFolder} disabled={isImporting || !folderPath.trim()} - className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" + className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" > - {isImporting ? 'Scanning...' : 'Start Scan'} + {isImporting ? 'Scanning...' : 'Start Scanning'} diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index e1a13d1..ee656d5 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -237,20 +237,36 @@ export default function Search() { }, [showTagModal, selectedPhotos.size]) // Get all unique tags from selected photos + // Only include tags that are single (linkage_type = 0) in ALL selected photos + // Tags that are bulk (linkage_type = 1) in ANY photo cannot be removed const allPhotoTags = useMemo(() => { + if (selectedPhotos.size === 0) return [] + + const photoIds = Array.from(selectedPhotos) const tagMap = new Map() + // First, collect all tags from all photos Object.values(photoTags).forEach(tags => { tags.forEach(tag => { - // Use tag_id as key to avoid duplicates if (!tagMap.has(tag.tag_id)) { tagMap.set(tag.tag_id, tag) } }) }) - return Array.from(tagMap.values()) - }, [photoTags]) + // Filter to only include tags that are single (linkage_type = 0) in ALL photos + const removableTags = Array.from(tagMap.values()).filter(tag => { + // Check if this tag is single in all selected photos + return photoIds.every(photoId => { + const photoTagList = photoTags[photoId] || [] + const photoTag = photoTagList.find(t => t.tag_id === tag.tag_id) + // Tag must exist in photo and be single (linkage_type = 0) + return photoTag && photoTag.linkage_type === 0 + }) + }) + + return removableTags + }, [photoTags, selectedPhotos]) const handleAddTag = async () => { if (selectedPhotos.size === 0) { @@ -311,7 +327,18 @@ export default function Search() { setShowDeleteConfirm(false) // Get tag names from selected tag IDs + // Filter to only include tags that are single (linkage_type = 0) in all photos + // This is a defensive check - allPhotoTags should already only contain single tags + const photoIds = Array.from(selectedPhotos) const tagNamesToDelete = Array.from(selectedTagIds) + .filter(tagId => { + // Check if tag is single in all selected photos + return photoIds.every(photoId => { + const photoTagList = photoTags[photoId] || [] + const photoTag = photoTagList.find(t => t.tag_id === tagId) + return photoTag && photoTag.linkage_type === 0 + }) + }) .map(tagId => { const tag = allPhotoTags.find(t => t.tag_id === tagId) return tag ? tag.tag_name : null @@ -319,7 +346,7 @@ export default function Search() { .filter(Boolean) as string[] if (tagNamesToDelete.length === 0) { - alert('No valid tags selected for deletion.') + alert('No removable tags selected. Bulk tags cannot be removed from this dialog.') return } @@ -804,12 +831,15 @@ export default function Search() {
+

+ Note: Bulk tags cannot be removed from this dialog +

{loadingTags ? (

Loading tags...

) : allPhotoTags.length === 0 ? ( -

No tags on selected photos

+

No removable tags on selected photos

) : (
{allPhotoTags.map(tag => ( @@ -830,7 +860,6 @@ export default function Search() { /> {tag.tag_name} - {tag.linkage_type === 1 && ' (bulk)'}
))} diff --git a/frontend/src/pages/Tags.tsx b/frontend/src/pages/Tags.tsx index 039d208..6b84e13 100644 --- a/frontend/src/pages/Tags.tsx +++ b/frontend/src/pages/Tags.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react' import tagsApi, { PhotoWithTagsItem, TagResponse } from '../api/tags' +import { useDeveloperMode } from '../context/DeveloperModeContext' type ViewMode = 'list' | 'icons' | 'compact' @@ -22,6 +23,7 @@ interface FolderGroup { } export default function Tags() { + const { isDeveloperMode } = useDeveloperMode() const [viewMode, setViewMode] = useState('list') const [photos, setPhotos] = useState([]) const [tags, setTags] = useState([]) @@ -34,6 +36,8 @@ export default function Tags() { const [showManageTags, setShowManageTags] = useState(false) const [showTagDialog, setShowTagDialog] = useState(null) const [showBulkTagDialog, setShowBulkTagDialog] = useState(null) + const [selectedPhotoIds, setSelectedPhotoIds] = useState>(new Set()) + const [showTagSelectedDialog, setShowTagSelectedDialog] = useState(false) // Load photos and tags useEffect(() => { @@ -332,41 +336,50 @@ export default function Tags() {

Photo Explorer - Tag Management

-
- -
- - - + {isDeveloperMode && ( +
+ +
+ + + +
-
+ )} +