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
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:
parent
09ee8712aa
commit
b0c9ad8d5d
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user