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:
@@ -116,6 +116,28 @@ export const photos = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
/** Add the listed tags to every listed photo. Idempotent — re-adding
|
||||
* an existing (photo, tag) pair is a no-op. Returns { added: N }. */
|
||||
bulkAddTags: async (photoIds: string[], tagIds: string[]) => {
|
||||
const response = await api.post('/photos/bulk', {
|
||||
ids: photoIds,
|
||||
action: 'add_tags',
|
||||
value: tagIds,
|
||||
})
|
||||
return response.data as { status: string; added: number }
|
||||
},
|
||||
|
||||
/** Remove the listed tags from every listed photo. Removing a
|
||||
* non-member is a no-op. Returns { removed: N }. */
|
||||
bulkRemoveTags: async (photoIds: string[], tagIds: string[]) => {
|
||||
const response = await api.post('/photos/bulk', {
|
||||
ids: photoIds,
|
||||
action: 'remove_tags',
|
||||
value: tagIds,
|
||||
})
|
||||
return response.data as { status: string; removed: number }
|
||||
},
|
||||
|
||||
/** Move photos into a target folder (or source root). Returns
|
||||
* { moved, errors[] }. */
|
||||
move: async (photoIds: string[], targetId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user