From b0c9ad8d5d7a7f3458550ea75b04747156dac22b Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Thu, 5 Feb 2026 17:27:41 +0000 Subject: [PATCH] feat: enhance tag management in Tags page - 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. --- admin-frontend/src/pages/Tags.tsx | 135 +++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 31 deletions(-) diff --git a/admin-frontend/src/pages/Tags.tsx b/admin-frontend/src/pages/Tags.tsx index f139608..a349730 100644 --- a/admin-frontend/src/pages/Tags.tsx +++ b/admin-frontend/src/pages/Tags.tsx @@ -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 onClose: () => void }) { const [selectedTagName, setSelectedTagName] = useState('') + const [newTagName, setNewTagName] = useState('') const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [showConfirmDialog, setShowConfirmDialog] = useState(false) const [photoTagsData, setPhotoTagsData] = useState>({}) + const [localTags, setLocalTags] = useState(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 = {} @@ -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() @@ -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({

-
+
+ - +

+ You can select an existing tag and enter a new tag name to add both at once. +

+
+
+ + 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() + } + }} + /> +

+ New tags will be created in the database automatically. +

@@ -2024,12 +2088,21 @@ function TagSelectedPhotosDialog({ > Remove selected tags - +
+ + +