Skip to content

Commit c1d230f

Browse files
Mary Hipppsychedelicious
authored andcommitted
add support to delete all uncategorized images
1 parent 6810843 commit c1d230f

File tree

8 files changed

+529
-27
lines changed

8 files changed

+529
-27
lines changed

invokeai/app/api/routers/boards.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async def list_boards(
146146
response_model=list[str],
147147
)
148148
async def list_all_board_image_names(
149-
board_id: str = Path(description="The id of the board"),
149+
board_id: str = Path(description="The id of the board or 'none' for uncategorized images"),
150150
categories: list[ImageCategory] | None = Query(default=None, description="The categories of image to include."),
151151
is_intermediate: bool | None = Query(default=None, description="Whether to list intermediate images."),
152152
) -> list[str]:

invokeai/app/api/routers/images.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,29 @@ async def delete_images_from_list(
395395
raise HTTPException(status_code=500, detail="Failed to delete images")
396396

397397

398+
@images_router.delete(
399+
"/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesFromListResult
400+
)
401+
async def delete_uncategorized_images() -> DeleteImagesFromListResult:
402+
"""Deletes all images that are uncategorized"""
403+
404+
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
405+
board_id="none", categories=None, is_intermediate=None
406+
)
407+
408+
try:
409+
deleted_images: list[str] = []
410+
for image_name in image_names:
411+
try:
412+
ApiDependencies.invoker.services.images.delete(image_name)
413+
deleted_images.append(image_name)
414+
except Exception:
415+
pass
416+
return DeleteImagesFromListResult(deleted_images=deleted_images)
417+
except Exception:
418+
raise HTTPException(status_code=500, detail="Failed to delete images")
419+
420+
398421
class ImagesUpdatedFromListResult(BaseModel):
399422
updated_image_names: list[str] = Field(description="The image names that were updated")
400423

invokeai/app/services/board_image_records/board_image_records_sqlite.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,18 @@ def get_all_board_image_names_for_board(
9898
FROM images
9999
LEFT JOIN board_images ON board_images.image_name = images.image_name
100100
WHERE 1=1
101+
"""
102+
103+
# Handle board_id filter
104+
if board_id == "none":
105+
stmt += """--sql
106+
AND board_images.board_id IS NULL
107+
"""
108+
else:
109+
stmt += """--sql
101110
AND board_images.board_id = ?
102111
"""
103-
params.append(board_id)
112+
params.append(board_id)
104113

105114
# Add the category filter
106115
if categories is not None:

invokeai/frontend/web/public/locales/en.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@
2424
"autoAddBoard": "Auto-Add Board",
2525
"boards": "Boards",
2626
"selectedForAutoAdd": "Selected for Auto-Add",
27-
"bottomMessage": "Deleting this board and its images will reset any features currently using them.",
27+
"bottomMessage": "Deleting images will reset any features currently using them.",
2828
"cancel": "Cancel",
2929
"changeBoard": "Change Board",
3030
"clearSearch": "Clear Search",
3131
"deleteBoard": "Delete Board",
3232
"deleteBoardAndImages": "Delete Board and Images",
3333
"deleteBoardOnly": "Delete Board Only",
34-
"deletedBoardsCannotbeRestored": "Deleted boards cannot be restored. Selecting 'Delete Board Only' will move images to an uncategorized state.",
35-
"deletedPrivateBoardsCannotbeRestored": "Deleted boards cannot be restored. Selecting 'Delete Board Only' will move images to a private uncategorized state for the image's creator.",
34+
"deletedBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to an uncategorized state.",
35+
"deletedPrivateBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to a private uncategorized state for the image's creator.",
36+
"uncategorizedImages": "Uncategorized Images",
37+
"deleteAllUncategorizedImages": "Delete All Uncategorized Images",
38+
"deletedImagesCannotBeRestored": "Deleted images cannot be restored.",
3639
"hideBoards": "Hide Boards",
3740
"loading": "Loading...",
3841
"menuItemAutoAdd": "Auto-add to this Board",
@@ -46,7 +49,7 @@
4649
"searchBoard": "Search Boards...",
4750
"selectBoard": "Select a Board",
4851
"shared": "Shared Boards",
49-
"topMessage": "This board contains images used in the following features:",
52+
"topMessage": "This selection contains images used in the following features:",
5053
"unarchiveBoard": "Unarchive Board",
5154
"uncategorized": "Uncategorized",
5255
"viewBoards": "View Boards",

invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,26 @@ import { atom } from 'nanostores';
2626
import { memo, useCallback, useMemo, useRef } from 'react';
2727
import { useTranslation } from 'react-i18next';
2828
import { useListAllImageNamesForBoardQuery } from 'services/api/endpoints/boards';
29-
import { useDeleteBoardAndImagesMutation, useDeleteBoardMutation } from 'services/api/endpoints/images';
29+
import {
30+
useDeleteBoardAndImagesMutation,
31+
useDeleteBoardMutation,
32+
useDeleteUncategorizedImagesMutation,
33+
} from 'services/api/endpoints/images';
3034
import type { BoardDTO } from 'services/api/types';
3135

32-
export const $boardToDelete = atom<BoardDTO | null>(null);
36+
export const $boardToDelete = atom<BoardDTO | 'none' | null>(null);
3337

3438
const DeleteBoardModal = () => {
3539
useAssertSingleton('DeleteBoardModal');
3640
const boardToDelete = useStore($boardToDelete);
3741
const { t } = useTranslation();
42+
43+
const boardId = useMemo(() => (boardToDelete === 'none' ? 'none' : boardToDelete?.board_id), [boardToDelete]);
44+
3845
const { currentData: boardImageNames, isFetching: isFetchingBoardNames } = useListAllImageNamesForBoardQuery(
39-
boardToDelete?.board_id
46+
boardId
4047
? {
41-
board_id: boardToDelete?.board_id,
48+
board_id: boardId,
4249
categories: undefined,
4350
is_intermediate: undefined,
4451
}
@@ -71,33 +78,48 @@ const DeleteBoardModal = () => {
7178

7279
const [deleteBoardAndImages, { isLoading: isDeleteBoardAndImagesLoading }] = useDeleteBoardAndImagesMutation();
7380

81+
const [deleteUncategorizedImages, { isLoading: isDeleteUncategorizedImagesLoading }] =
82+
useDeleteUncategorizedImagesMutation();
83+
7484
const imageUsageSummary = useAppSelector(selectImageUsageSummary);
7585

7686
const handleDeleteBoardOnly = useCallback(() => {
77-
if (!boardToDelete) {
87+
if (!boardToDelete || boardToDelete === 'none') {
7888
return;
7989
}
8090
deleteBoardOnly(boardToDelete.board_id);
8191
$boardToDelete.set(null);
8292
}, [boardToDelete, deleteBoardOnly]);
8393

8494
const handleDeleteBoardAndImages = useCallback(() => {
85-
if (!boardToDelete) {
95+
if (!boardToDelete || boardToDelete === 'none') {
8696
return;
8797
}
8898
deleteBoardAndImages(boardToDelete.board_id);
8999
$boardToDelete.set(null);
90100
}, [boardToDelete, deleteBoardAndImages]);
91101

102+
const handleDeleteUncategorizedImages = useCallback(() => {
103+
if (!boardToDelete || boardToDelete !== 'none') {
104+
return;
105+
}
106+
deleteUncategorizedImages();
107+
$boardToDelete.set(null);
108+
}, [boardToDelete, deleteUncategorizedImages]);
109+
92110
const handleClose = useCallback(() => {
93111
$boardToDelete.set(null);
94112
}, []);
95113

96114
const cancelRef = useRef<HTMLButtonElement>(null);
97115

98116
const isLoading = useMemo(
99-
() => isDeleteBoardAndImagesLoading || isDeleteBoardOnlyLoading || isFetchingBoardNames,
100-
[isDeleteBoardAndImagesLoading, isDeleteBoardOnlyLoading, isFetchingBoardNames]
117+
() =>
118+
isDeleteBoardAndImagesLoading ||
119+
isDeleteBoardOnlyLoading ||
120+
isFetchingBoardNames ||
121+
isDeleteUncategorizedImagesLoading,
122+
[isDeleteBoardAndImagesLoading, isDeleteBoardOnlyLoading, isFetchingBoardNames, isDeleteUncategorizedImagesLoading]
101123
);
102124

103125
if (!boardToDelete) {
@@ -109,7 +131,7 @@ const DeleteBoardModal = () => {
109131
<AlertDialogOverlay>
110132
<AlertDialogContent>
111133
<AlertDialogHeader fontSize="lg" fontWeight="bold">
112-
{t('common.delete')} {boardToDelete.board_name}
134+
{t('common.delete')} {boardToDelete === 'none' ? t('boards.uncategorizedImages') : boardToDelete.board_name}
113135
</AlertDialogHeader>
114136

115137
<AlertDialogBody>
@@ -126,9 +148,10 @@ const DeleteBoardModal = () => {
126148
/>
127149
)}
128150
<Text>
129-
{boardToDelete.is_private
130-
? t('boards.deletedPrivateBoardsCannotbeRestored')
131-
: t('boards.deletedBoardsCannotbeRestored')}
151+
{boardToDelete !== 'none' &&
152+
(boardToDelete.is_private
153+
? t('boards.deletedPrivateBoardsCannotbeRestored')
154+
: t('boards.deletedBoardsCannotbeRestored'))}
132155
</Text>
133156
<Text>{t('gallery.deleteImagePermanent')}</Text>
134157
</Flex>
@@ -138,12 +161,21 @@ const DeleteBoardModal = () => {
138161
<Button ref={cancelRef} onClick={handleClose}>
139162
{t('boards.cancel')}
140163
</Button>
141-
<Button colorScheme="warning" isLoading={isLoading} onClick={handleDeleteBoardOnly}>
142-
{t('boards.deleteBoardOnly')}
143-
</Button>
144-
<Button colorScheme="error" isLoading={isLoading} onClick={handleDeleteBoardAndImages}>
145-
{t('boards.deleteBoardAndImages')}
146-
</Button>
164+
{boardToDelete !== 'none' && (
165+
<Button colorScheme="warning" isLoading={isLoading} onClick={handleDeleteBoardOnly}>
166+
{t('boards.deleteBoardOnly')}
167+
</Button>
168+
)}
169+
{boardToDelete !== 'none' && (
170+
<Button colorScheme="error" isLoading={isLoading} onClick={handleDeleteBoardAndImages}>
171+
{t('boards.deleteBoardAndImages')}
172+
</Button>
173+
)}
174+
{boardToDelete === 'none' && (
175+
<Button colorScheme="error" isLoading={isLoading} onClick={handleDeleteUncategorizedImages}>
176+
{t('boards.deleteAllUncategorizedImages')}
177+
</Button>
178+
)}
147179
</Flex>
148180
</AlertDialogFooter>
149181
</AlertDialogContent>

invokeai/frontend/web/src/features/gallery/components/Boards/NoBoardBoardContextMenu.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
77
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
88
import { memo, useCallback } from 'react';
99
import { useTranslation } from 'react-i18next';
10-
import { PiDownloadBold, PiPlusBold } from 'react-icons/pi';
10+
import { PiDownloadBold, PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
1111
import { useBulkDownloadImagesMutation } from 'services/api/endpoints/images';
1212

13+
import { $boardToDelete } from './DeleteBoardModal';
14+
1315
type Props = {
1416
children: ContextMenuProps<HTMLDivElement>['children'];
1517
};
@@ -33,6 +35,10 @@ const NoBoardBoardContextMenu = ({ children }: Props) => {
3335
bulkDownload({ image_names: [], board_id: 'none' });
3436
}, [bulkDownload]);
3537

38+
const setUncategorizedImagesAsToBeDeleted = useCallback(() => {
39+
$boardToDelete.set('none');
40+
}, []);
41+
3642
const renderMenuFunc = useCallback(
3743
() => (
3844
<MenuList visibility="visible">
@@ -47,10 +53,26 @@ const NoBoardBoardContextMenu = ({ children }: Props) => {
4753
{t('boards.downloadBoard')}
4854
</MenuItem>
4955
)}
56+
<MenuItem
57+
color="error.300"
58+
icon={<PiTrashSimpleBold />}
59+
onClick={setUncategorizedImagesAsToBeDeleted}
60+
isDestructive
61+
>
62+
{t('boards.deleteAllUncategorizedImages')}
63+
</MenuItem>
5064
</MenuGroup>
5165
</MenuList>
5266
),
53-
[autoAssignBoardOnClick, handleBulkDownload, handleSetAutoAdd, isBulkDownloadEnabled, isSelectedForAutoAdd, t]
67+
[
68+
autoAssignBoardOnClick,
69+
handleBulkDownload,
70+
handleSetAutoAdd,
71+
isBulkDownloadEnabled,
72+
isSelectedForAutoAdd,
73+
t,
74+
setUncategorizedImagesAsToBeDeleted,
75+
]
5476
);
5577

5678
return <ContextMenu renderMenu={renderMenuFunc}>{children}</ContextMenu>;

invokeai/frontend/web/src/services/api/endpoints/images.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,42 @@ export const imagesApi = api.injectEndpoints({
160160
return [];
161161
},
162162
}),
163+
deleteUncategorizedImages: build.mutation<components['schemas']['DeleteImagesFromListResult'], void>({
164+
query: () => ({ url: buildImagesUrl('uncategorized'), method: 'DELETE' }),
165+
invalidatesTags: (result) => {
166+
if (result && result.deleted_images.length > 0) {
167+
const boardId = 'none';
168+
169+
const tags: ApiTagDescription[] = [
170+
{
171+
type: 'ImageList',
172+
id: getListImagesUrl({
173+
board_id: boardId,
174+
categories: IMAGE_CATEGORIES,
175+
}),
176+
},
177+
{
178+
type: 'ImageList',
179+
id: getListImagesUrl({
180+
board_id: boardId,
181+
categories: ASSETS_CATEGORIES,
182+
}),
183+
},
184+
{
185+
type: 'Board',
186+
id: boardId,
187+
},
188+
{
189+
type: 'BoardImagesTotal',
190+
id: boardId,
191+
},
192+
];
193+
194+
return tags;
195+
}
196+
return [];
197+
},
198+
}),
163199
/**
164200
* Change an image's `is_intermediate` property.
165201
*/
@@ -566,6 +602,7 @@ export const {
566602
useAddImagesToBoardMutation,
567603
useRemoveImagesFromBoardMutation,
568604
useDeleteBoardAndImagesMutation,
605+
useDeleteUncategorizedImagesMutation,
569606
useDeleteBoardMutation,
570607
useStarImagesMutation,
571608
useUnstarImagesMutation,

0 commit comments

Comments
 (0)