feat: enhance tag management in Tags page
All checks were successful
CI / skip-ci-check (pull_request) Successful in 10s
CI / lint-and-type-check (pull_request) Successful in 56s
CI / python-lint (pull_request) Successful in 35s
CI / test-backend (pull_request) Successful in 3m44s
CI / build (pull_request) Successful in 3m26s
CI / secret-scanning (pull_request) Successful in 15s
CI / dependency-scan (pull_request) Successful in 16s
CI / sast-scan (pull_request) Successful in 1m29s
CI / workflow-summary (pull_request) Successful in 6s

- Added functionality to create new tags and update existing tags in bulk.
- Implemented local state management for tags to improve user experience.
- Updated UI to allow users to enter new tag names alongside selecting existing ones.
- Ensured tags are reloaded in the parent component after creation for synchronization.
This commit is contained in:
tanyar09 2026-02-05 17:27:41 +00:00
parent 09ee8712aa
commit b0c9ad8d5d

View File

@ -1115,6 +1115,11 @@ export default function Tags() {
selectedPhotoIds={Array.from(selectedPhotoIds)}
photos={photos.filter(p => selectedPhotoIds.has(p.id))}
tags={tags}
onTagsUpdated={async () => {
// Reload tags when new tags are created
const tagsRes = await tagsApi.list()
setTags(tagsRes.items)
}}
onClose={async () => {
setShowTagSelectedDialog(false)
setSelectedPhotoIds(new Set())
@ -1775,17 +1780,26 @@ function TagSelectedPhotosDialog({
selectedPhotoIds,
photos,
tags,
onTagsUpdated,
onClose,
}: {
selectedPhotoIds: number[]
photos: PhotoWithTagsItem[]
tags: TagResponse[]
onTagsUpdated?: () => Promise<void>
onClose: () => void
}) {
const [selectedTagName, setSelectedTagName] = useState('')
const [newTagName, setNewTagName] = useState('')
const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set())
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [photoTagsData, setPhotoTagsData] = useState<Record<number, any[]>>({})
const [localTags, setLocalTags] = useState<TagResponse[]>(tags)
// Update local tags when tags prop changes
useEffect(() => {
setLocalTags(tags)
}, [tags])
// Load tag linkage information for all selected photos
useEffect(() => {
@ -1809,28 +1823,59 @@ function TagSelectedPhotosDialog({
}, [selectedPhotoIds])
const handleAddTag = async () => {
if (!selectedTagName.trim() || selectedPhotoIds.length === 0) return
if (selectedPhotoIds.length === 0) return
// Check if tag exists, create if not
let tag = tags.find(t => t.tag_name.toLowerCase() === selectedTagName.toLowerCase().trim())
if (!tag) {
try {
tag = await tagsApi.create(selectedTagName.trim())
// Note: We don't update the tags list here since it's passed from parent
} catch (error) {
console.error('Failed to create tag:', error)
alert('Failed to create tag')
return
}
// Collect both tags: selected existing tag and new tag name
const tagsToAdd: string[] = []
if (selectedTagName.trim()) {
tagsToAdd.push(selectedTagName.trim())
}
if (newTagName.trim()) {
tagsToAdd.push(newTagName.trim())
}
if (tagsToAdd.length === 0) {
alert('Please select a tag or enter a new tag name.')
return
}
// Make single batch API call for all selected photos
try {
// Create any new tags first
const newTags = tagsToAdd.filter(tag =>
!localTags.some(availableTag =>
availableTag.tag_name.toLowerCase() === tag.toLowerCase()
)
)
if (newTags.length > 0) {
const createdTags: TagResponse[] = []
for (const newTag of newTags) {
const createdTag = await tagsApi.create(newTag)
createdTags.push(createdTag)
}
// Update local tags immediately with newly created tags
setLocalTags(prev => {
const updated = [...prev, ...createdTags]
// Sort by tag name
return updated.sort((a, b) => a.tag_name.localeCompare(b.tag_name))
})
// Also reload tags list in parent to keep it in sync
if (onTagsUpdated) {
await onTagsUpdated()
}
}
// Add all tags to photos in a single API call
await tagsApi.addToPhotos({
photo_ids: selectedPhotoIds,
tag_names: [selectedTagName.trim()],
tag_names: tagsToAdd,
})
// Clear inputs after successful tagging
setSelectedTagName('')
setNewTagName('')
// Reload photo tags data to update the common tags list
const tagsData: Record<number, any[]> = {}
@ -1901,7 +1946,7 @@ function TagSelectedPhotosDialog({
allPhotoTags[photoId] = photoTagsData[photoId] || []
})
const tagIdToName = new Map(tags.map(t => [t.id, t.tag_name]))
const tagIdToName = new Map(localTags.map(t => [t.id, t.tag_name]))
// Get all unique tag IDs from all photos
const allTagIds = new Set<number>()
@ -1930,7 +1975,7 @@ function TagSelectedPhotosDialog({
}
})
.filter(Boolean) as any[]
}, [photos, tags, selectedPhotoIds, photoTagsData])
}, [photos, localTags, selectedPhotoIds, photoTagsData])
// Get selected tag names for confirmation message
const selectedTagNames = useMemo(() => {
@ -1961,11 +2006,14 @@ function TagSelectedPhotosDialog({
</p>
</div>
<div className="p-4 flex-1 overflow-auto">
<div className="mb-4 flex gap-2">
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Existing Tag:
</label>
<select
value={selectedTagName}
onChange={(e) => setSelectedTagName(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded"
className="w-full px-3 py-2 border border-gray-300 rounded"
>
<option value="">Select tag...</option>
{tags.map(tag => (
@ -1974,13 +2022,29 @@ function TagSelectedPhotosDialog({
</option>
))}
</select>
<button
onClick={handleAddTag}
disabled={!selectedTagName.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Add
</button>
<p className="text-xs text-gray-500 mt-1">
You can select an existing tag and enter a new tag name to add both at once.
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Enter New Tag Name:
</label>
<input
type="text"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded"
placeholder="Type new tag name..."
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAddTag()
}
}}
/>
<p className="text-xs text-gray-500 mt-1">
New tags will be created in the database automatically.
</p>
</div>
<div className="space-y-2">
@ -2024,12 +2088,21 @@ function TagSelectedPhotosDialog({
>
Remove selected tags
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
>
Close
</button>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
>
Close
</button>
<button
onClick={handleAddTag}
disabled={!selectedTagName.trim() && !newTagName.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Add Tag
</button>
</div>
</div>
</div>
</div>