feat: bulk tag add/remove on multi-select right sidebar
The bulk action panel previously covered rating, color, flag, and pick but had no way to apply tags across a multi-photo selection — the only path was to tag photos one at a time via the single-photo PhotoInfoPanel. Add it. - backend: extend the existing /photos/bulk action endpoint with add_tags and remove_tags actions. add_tags is idempotent (computes the new (photo_id, tag_id) pair set against existing rows and inserts only the missing ones); remove_tags is a single DELETE WHERE IN. - api.ts: bulkAddTags / bulkRemoveTags wrappers. - RightSidebar: new BulkTagsEditor below the bulk flag row. Filters / searches the existing tag list, lets the user click any chip to apply it to the whole selection or X to remove it. Typing a name with no exact match shows a "Create and apply" button that creates the tag via tagsApi.create and immediately attaches it to every selected photo. All three mutations invalidate both the photo and tag caches so the FilterBar tag count + sidebar Tags section stay fresh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -862,6 +862,54 @@ async def bulk_action(
|
||||
elif action.action == 'set_color':
|
||||
for photo in photos:
|
||||
photo.color_label = action.value
|
||||
elif action.action == 'add_tags':
|
||||
# value is a list of tag ids. We bulk-insert (photo_id, tag_id)
|
||||
# rows for every (photo, tag) combination that doesn't already
|
||||
# exist, so the operation is idempotent.
|
||||
tag_ids = action.value or []
|
||||
if not isinstance(tag_ids, list) or not tag_ids:
|
||||
return {"status": "success", "added": 0, "message": "No tags supplied"}
|
||||
photo_ids = [p.id for p in photos]
|
||||
existing = await db.execute(
|
||||
select(photo_tags.c.photo_id, photo_tags.c.tag_id).where(
|
||||
photo_tags.c.photo_id.in_(photo_ids),
|
||||
photo_tags.c.tag_id.in_(tag_ids),
|
||||
)
|
||||
)
|
||||
existing_pairs = {(row[0], row[1]) for row in existing.all()}
|
||||
new_rows = [
|
||||
{"photo_id": pid, "tag_id": tid}
|
||||
for pid in photo_ids
|
||||
for tid in tag_ids
|
||||
if (pid, tid) not in existing_pairs
|
||||
]
|
||||
if new_rows:
|
||||
from sqlalchemy import insert
|
||||
await db.execute(insert(photo_tags), new_rows)
|
||||
await db.commit()
|
||||
return {
|
||||
"status": "success",
|
||||
"added": len(new_rows),
|
||||
"message": f"Added {len(new_rows)} tag link{'s' if len(new_rows) != 1 else ''}",
|
||||
}
|
||||
elif action.action == 'remove_tags':
|
||||
tag_ids = action.value or []
|
||||
if not isinstance(tag_ids, list) or not tag_ids:
|
||||
return {"status": "success", "removed": 0, "message": "No tags supplied"}
|
||||
photo_ids = [p.id for p in photos]
|
||||
from sqlalchemy import delete as sql_delete
|
||||
result = await db.execute(
|
||||
sql_delete(photo_tags).where(
|
||||
photo_tags.c.photo_id.in_(photo_ids),
|
||||
photo_tags.c.tag_id.in_(tag_ids),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
return {
|
||||
"status": "success",
|
||||
"removed": result.rowcount or 0,
|
||||
"message": f"Removed tag link{'s' if (result.rowcount or 0) != 1 else ''}",
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid action")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user