diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0460ab34fe8..9c1629a8e36 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -50,6 +50,8 @@ "selectBoard": "Select a Board", "shared": "Shared Boards", "topMessage": "This selection contains images used in the following features:", + "containsStarredTitle": "Warning: Starred images", + "containsStarredConfirm": "This selection contains starred images. Delete anyway? This cannot be undone.", "unarchiveBoard": "Unarchive Board", "uncategorized": "Uncategorized", "viewBoards": "View Boards", @@ -354,6 +356,7 @@ "boardsSettings": "Boards Settings", "copy": "Copy", "currentlyInUse": "This image is currently in use in the following features:", + "cannotDeleteStarred": "Starred images are protected — remove the star or disable protection.", "drop": "Drop", "dropOrUpload": "Drop or Upload", "dropToUpload": "$t(gallery.drop) to Upload", @@ -1335,6 +1338,8 @@ "antialiasProgressImages": "Antialias Progress Images", "beta": "Beta", "confirmOnDelete": "Confirm On Delete", + "protectStarredImages": "Protect starred images", + "protectStarredImagesDesc1": "In case the image have the Star mark, this option will protect it from accidental deletion", "confirmOnNewSession": "Confirm On New Session", "developer": "Developer", "displayInProgress": "Display Progress Images", diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 93783e559d8..205cee8f264 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -126,6 +126,7 @@ "bulkDownloadRequested": "Подготовка к скачиванию", "bulkDownloadRequestedDesc": "Ваш запрос на скачивание готовится. Это может занять несколько минут.", "bulkDownloadRequestFailed": "Возникла проблема при подготовке скачивания", + "cannotDeleteStarred": "Изображения, отмеченные звездочкой, защищены от удаления. Снимите отметку чтобы удалить.", "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения", "openInViewer": "Открыть в просмотрщике", "selectForCompare": "Выбрать для сравнения", @@ -690,6 +691,8 @@ "models": "Модели", "displayInProgress": "Показывать процесс генерации", "confirmOnDelete": "Подтверждать удаление", + "protectStarredImages": "Защитить помеченные изображения", + "protectStarredImagesDesc1": "Если изображение помечено звездочкой - эта опция защищает его от случайного удаления", "resetWebUI": "Сброс настроек веб-интерфейса", "resetWebUIDesc1": "Сброс настроек веб-интерфейса удаляет только локальный кэш браузера с вашими изображениями и настройками. Он не удаляет изображения с диска.", "resetWebUIDesc2": "Если изображения не отображаются в галерее или не работает что-то еще, пожалуйста, попробуйте сбросить настройки, прежде чем сообщать о проблеме на GitHub.", @@ -963,6 +966,8 @@ "deleteBoard": "Удалить доску", "deleteBoardAndImages": "Удалить доску и изображения", "deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.", + "containsStarredTitle": "Внимание: Помеченные изображения", + "containsStarredConfirm": "Эта доска содержит помеченные изображения. Вы уверены, что хотите продолжить? Это действие необратимо.", "assetsWithCount_one": "{{count}} актив", "assetsWithCount_few": "{{count}} актива", "assetsWithCount_many": "{{count}} активов", diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts index 0c19eda02e3..ad4dc10a4ed 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts @@ -11,7 +11,7 @@ import { import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; -import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { selectGetImageNamesQueryArgs, selectImageByName } from 'features/gallery/store/gallerySelectors'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/selectors'; @@ -19,7 +19,12 @@ import type { NodesState } from 'features/nodes/store/types'; import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectUpscaleSlice, type UpscaleState } from 'features/parameters/store/upscaleSlice'; -import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice'; +import { + selectSystemShouldConfirmOnDelete, + selectSystemShouldProtectStarredImages, +} from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; import { atom } from 'nanostores'; import { useMemo } from 'react'; import { imagesApi } from 'services/api/endpoints/images'; @@ -57,6 +62,26 @@ const deleteImagesWithDialog = async (image_names: string[], store: AppStore): P const { getState } = store; const imageUsage = getImageUsageFromImageNames(image_names, getState()); const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState()); + const shouldProtectStarred = selectSystemShouldProtectStarredImages(getState()); + + if (shouldProtectStarred) { + // find which of the incoming names are starred + const starred = image_names.filter((name) => selectImageByName(getState(), name)?.starred); + if (starred.length) { + if (selectSystemShouldConfirmOnDelete(getState())) { + // show toast explaining why we refuse only if we are not in "silent mode" + toast({ + status: 'warning', + title: t('gallery.cannotDeleteStarred'), + }); + } + + image_names = image_names.filter((n) => !starred.includes(n)); + if (!image_names.length) { + return; + } + } + } if (!shouldConfirmOnDelete && !isAnyImageInUse(imageUsage)) { // If we don't need to confirm and the images are not in use, delete them directly diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 47d59540f1d..e315afddefc 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -6,6 +6,7 @@ import { AlertDialogHeader, AlertDialogOverlay, Button, + ConfirmationAlertDialog, Flex, Skeleton, Text, @@ -13,6 +14,7 @@ import { import { useStore } from '@nanostores/react'; import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { getStore } from 'app/store/nanostores/store'; import { useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { some } from 'es-toolkit/compat'; @@ -21,15 +23,23 @@ import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/state'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; +import { useBoardContainsStarred } from 'features/gallery/hooks/useBoardContainsStarred'; +import { selectImageByName } from 'features/gallery/store/gallerySelectors'; import { selectNodesSlice } from 'features/nodes/store/selectors'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; +import { + selectSystemShouldConfirmOnDelete, + selectSystemShouldProtectStarredImages, +} from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { atom } from 'nanostores'; -import { memo, useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useListAllImageNamesForBoardQuery } from 'services/api/endpoints/boards'; import { useDeleteBoardAndImagesMutation, useDeleteBoardMutation, + useDeleteImagesMutation, useDeleteUncategorizedImagesMutation, } from 'services/api/endpoints/images'; import type { BoardDTO } from 'services/api/types'; @@ -40,9 +50,22 @@ const DeleteBoardModal = () => { useAssertSingleton('DeleteBoardModal'); const boardToDelete = useStore($boardToDelete); const { t } = useTranslation(); + // retrieve accidental deletion protection option from app config + const shouldProtectStarred = useAppSelector(selectSystemShouldProtectStarredImages); + + // we will also need to know if deletion confirmations are enabled + const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete); const boardId = useMemo(() => (boardToDelete === 'none' ? 'none' : boardToDelete?.board_id), [boardToDelete]); + const { isChecking, containsStarred } = useBoardContainsStarred( + boardToDelete && boardToDelete !== 'none' ? boardId : undefined, + shouldProtectStarred + ); + + const { isChecking: isCheckingUncategorized, containsStarred: containsStarredUncategorized } = + useBoardContainsStarred(boardToDelete && boardToDelete === 'none' ? boardId : undefined, shouldProtectStarred); + const { currentData: boardImageNames, isFetching: isFetchingBoardNames } = useListAllImageNamesForBoardQuery( boardId ? { @@ -78,6 +101,9 @@ const DeleteBoardModal = () => { [boardImageNames] ); + const [isStarredConfirmOpen, setIsStarredConfirmOpen] = useState(false); + const pendingDeleteRef = useRef<() => void>(() => {}); + const [deleteBoardOnly, { isLoading: isDeleteBoardOnlyLoading }] = useDeleteBoardMutation(); const [deleteBoardAndImages, { isLoading: isDeleteBoardAndImagesLoading }] = useDeleteBoardAndImagesMutation(); @@ -85,6 +111,8 @@ const DeleteBoardModal = () => { const [deleteUncategorizedImages, { isLoading: isDeleteUncategorizedImagesLoading }] = useDeleteUncategorizedImagesMutation(); + const [deleteImages, { isLoading: isDeleteImagesLoading }] = useDeleteImagesMutation(); + const imageUsageSummary = useAppSelector(selectImageUsageSummary); const handleDeleteBoardOnly = useCallback(() => { @@ -95,26 +123,111 @@ const DeleteBoardModal = () => { $boardToDelete.set(null); }, [boardToDelete, deleteBoardOnly]); - const handleDeleteBoardAndImages = useCallback(() => { - if (!boardToDelete || boardToDelete === 'none') { + const performDelete = useCallback(() => { + if (!boardId) { return; } - deleteBoardAndImages({ board_id: boardToDelete.board_id }); + deleteBoardAndImages({ board_id: boardId }); $boardToDelete.set(null); - }, [boardToDelete, deleteBoardAndImages]); + }, [boardId, deleteBoardAndImages]); + + const finishStarredProtectedDelete = useCallback(() => { + pendingDeleteRef.current?.(); + setIsStarredConfirmOpen(false); + }, []); + + const handleDeleteBoardAndImages = useCallback(() => { + if (!boardId) { + return; + } + if (shouldProtectStarred) { + if (isChecking) { + return; + } + if (containsStarred) { + pendingDeleteRef.current = performDelete; + setIsStarredConfirmOpen(true); + return; + } + } + performDelete(); + }, [boardId, shouldProtectStarred, isChecking, containsStarred, performDelete]); const handleDeleteUncategorizedImages = useCallback(() => { if (!boardToDelete || boardToDelete !== 'none') { return; } + + // here we will check if there are starred images within the "uncategorized" board + if (shouldProtectStarred) { + if (isCheckingUncategorized) { + return; // frontend is still checking, no actions for now + } + + if (containsStarredUncategorized) { + const { getState } = getStore(); + const state = getState(); + + // now we will sieve through the uncategorized board to separate starred images from the rest + const starredNames = (boardImageNames ?? []).filter((name) => selectImageByName(state, name)?.starred); + + if (starredNames.length > 0) { + // the toast should appear only if delete confirmation is enabled, that's the idea + if (shouldConfirmOnDelete) { + toast({ + status: 'warning', + title: t('gallery.cannotDeleteStarred'), + }); + } + + // now we will delete all the images that are not bearing the star mark + const namesToDelete = (boardImageNames ?? []).filter((n) => !starredNames.includes(n)); + + if (!namesToDelete.length) { + $boardToDelete.set(null); + return; + } + + deleteImages({ image_names: namesToDelete }); + $boardToDelete.set(null); + return; + } else { + // in case all the images are starred, we will only throw a toast. If there's deletion confirmations, that is + if (shouldConfirmOnDelete) { + toast({ + status: 'warning', + title: t('gallery.cannotDeleteStarred'), + }); + } + $boardToDelete.set(null); + return; + } + } + } + + // fallback to standard behavior deleteUncategorizedImages(); $boardToDelete.set(null); - }, [boardToDelete, deleteUncategorizedImages]); + }, [ + boardToDelete, + shouldProtectStarred, + shouldConfirmOnDelete, + isCheckingUncategorized, + containsStarredUncategorized, + boardImageNames, + deleteImages, + deleteUncategorizedImages, + t, + ]); const handleClose = useCallback(() => { $boardToDelete.set(null); }, []); + const closeConfirmationAlertDlg = useCallback(() => { + setIsStarredConfirmOpen(false); + }, []); + const cancelRef = useRef(null); const isLoading = useMemo( @@ -122,70 +235,100 @@ const DeleteBoardModal = () => { isDeleteBoardAndImagesLoading || isDeleteBoardOnlyLoading || isFetchingBoardNames || + isDeleteUncategorizedImagesLoading || + isDeleteImagesLoading, + [ + isDeleteBoardAndImagesLoading, + isDeleteBoardOnlyLoading, + isFetchingBoardNames, isDeleteUncategorizedImagesLoading, - [isDeleteBoardAndImagesLoading, isDeleteBoardOnlyLoading, isFetchingBoardNames, isDeleteUncategorizedImagesLoading] + isDeleteImagesLoading, + ] ); if (!boardToDelete) { return null; } + let bOpenMainAlertDialog = Boolean(boardToDelete) && !isStarredConfirmOpen; + return ( - - - - - {t('common.delete')} {boardToDelete === 'none' ? t('boards.uncategorizedImages') : boardToDelete.board_name} - - - - - {isFetchingBoardNames ? ( - - - - ) : ( - - )} - {boardToDelete !== 'none' && ( - - {boardToDelete.is_private - ? t('boards.deletedPrivateBoardsCannotbeRestored') - : t('boards.deletedBoardsCannotbeRestored')} - - )} - {t('gallery.deleteImagePermanent')} - - - - - - {boardToDelete !== 'none' && ( - - )} - {boardToDelete !== 'none' && ( - - )} - {boardToDelete === 'none' && ( - - )} - - - - - + {boardToDelete !== 'none' && ( + + )} + {boardToDelete !== 'none' && ( + + )} + {boardToDelete === 'none' && ( + + )} + + + + + + + {t('boards.containsStarredConfirm')} + + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useBoardContainsStarred.ts b/invokeai/frontend/web/src/features/gallery/hooks/useBoardContainsStarred.ts new file mode 100644 index 00000000000..a389ed4a2c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useBoardContainsStarred.ts @@ -0,0 +1,21 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useGetImageNamesQuery } from 'services/api/endpoints/images'; + +/** + * Returns { isChecking, containsStarred } + * + * This helper is used to check whether the board has starred images and thus should be protected from accidental deletion + */ +export const useBoardContainsStarred = (boardId?: string, enabled = false) => { + const queryArgs = + enabled && boardId ? { board_id: boardId, starred_first: true, order_dir: 'DESC' as const } : skipToken; + // here we force "starred_first" option to true to populate "starred_count" value + // this should have no impact on user's board view preferences + + const { currentData, isFetching } = useGetImageNamesQuery(queryArgs); + + return { + isChecking: isFetching, // this will help us to wait some for the request to complete, not sure if it's necessary + containsStarred: (currentData?.starred_count ?? 0) > 0, + }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 490305f8afe..84d71b30cca 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -1,8 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import type { RootState } from 'app/store/store'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import type { GetImageNamesArgs, ListBoardsArgs } from 'services/api/types'; +import { imagesApi } from 'services/api/endpoints/images'; +import type { GetImageNamesArgs, ImageDTO, ListBoardsArgs } from 'services/api/types'; export const selectFirstSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(0)); export const selectLastSelectedImage = createSelector(selectGallerySlice, (gallery) => gallery.selection.at(-1)); @@ -67,3 +69,15 @@ export const selectAlwaysShouldImageSizeBadge = createSelector( selectGallerySlice, (gallery) => gallery.alwaysShowImageSizeBadge ); + +/** + * gets an ImageDTO for a given image_name from the RTK-Query cache + * thus we can get a bit more full image data without the need for additional queries over the net + */ +export const selectImageByName = (state: RootState, image_name: string | null | undefined): ImageDTO | null => { + if (!image_name) { + return null; + } + const { data } = imagesApi.endpoints.getImageDTO.select(image_name)(state); + return data ?? null; +}; diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 1be280fa40e..3a1e48d9b7b 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -33,6 +33,7 @@ import { selectSystemShouldEnableHighlightFocusedRegions, selectSystemShouldEnableInformationalPopovers, selectSystemShouldEnableModelDescriptions, + selectSystemShouldProtectStarredImages, selectSystemShouldShowInvocationProgressDetail, selectSystemShouldUseNSFWChecker, selectSystemShouldUseWatermarker, @@ -40,6 +41,7 @@ import { setShouldEnableInformationalPopovers, setShouldEnableModelDescriptions, setShouldHighlightFocusedRegions, + setShouldProtectStarredImages, setShouldShowInvocationProgressDetail, shouldAntialiasProgressImageChanged, shouldConfirmOnNewSessionToggled, @@ -102,6 +104,7 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise); const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete); + const shouldProtectStarredImages = useAppSelector(selectSystemShouldProtectStarredImages); const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); const shouldAntialiasProgressImage = useAppSelector(selectSystemShouldAntialiasProgressImage); const shouldUseNSFWChecker = useAppSelector(selectSystemShouldUseNSFWChecker); @@ -133,6 +136,14 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) }, [dispatch] ); + + const handleChangeProtectStarredImages = useCallback( + (e: ChangeEvent) => { + dispatch(setShouldProtectStarredImages(e.target.checked)); + }, + [dispatch] + ); + const handleChangeShouldUseNSFWChecker = useCallback( (e: ChangeEvent) => { dispatch(shouldUseNSFWCheckerChanged(e.target.checked)); @@ -209,6 +220,11 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) {t('settings.confirmOnDelete')} + + {t('settings.protectStarredImages')} + + + {t('settings.protectStarredImagesDesc1')} {t('settings.confirmOnNewSession')} diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 1cc22c8dea4..2961a50cad0 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -12,10 +12,11 @@ import { assert } from 'tsafe'; import { type Language, type SystemState, zSystemState } from './types'; const getInitialState = (): SystemState => ({ - _version: 2, + _version: 3, shouldConfirmOnDelete: true, shouldAntialiasProgressImage: false, shouldConfirmOnNewSession: true, + shouldProtectStarredImages: false, language: 'en', shouldUseNSFWChecker: false, shouldUseWatermarker: false, @@ -75,6 +76,9 @@ const slice = createSlice({ setShouldHighlightFocusedRegions(state, action: PayloadAction) { state.shouldHighlightFocusedRegions = action.payload; }, + setShouldProtectStarredImages(state, action: PayloadAction) { + state.shouldProtectStarredImages = action.payload; + }, }, }); @@ -92,6 +96,7 @@ export const { shouldConfirmOnNewSessionToggled, setShouldShowInvocationProgressDetail, setShouldHighlightFocusedRegions, + setShouldProtectStarredImages, } = slice.actions; export const systemSliceConfig: SliceConfig = { @@ -108,6 +113,12 @@ export const systemSliceConfig: SliceConfig = { state.language = (state as SystemState).language.replace('_', '-'); state._version = 2; } + // we could be leaving schema version as 2 as long as we are defaulting this new option anyway, + // but I feel this is more robust + if (state._version === 2) { + state.shouldProtectStarredImages = false; + state._version = 3; + } return zSystemState.parse(state); }, }, @@ -140,3 +151,4 @@ export const selectSystemShouldConfirmOnNewSession = createSystemSelector((syste export const selectSystemShouldShowInvocationProgressDetail = createSystemSelector( (system) => system.shouldShowInvocationProgressDetail ); +export const selectSystemShouldProtectStarredImages = createSystemSelector((s) => s.shouldProtectStarredImages); diff --git a/invokeai/frontend/web/src/features/system/store/types.ts b/invokeai/frontend/web/src/features/system/store/types.ts index 3eaf8628c61..82bbae914d1 100644 --- a/invokeai/frontend/web/src/features/system/store/types.ts +++ b/invokeai/frontend/web/src/features/system/store/types.ts @@ -30,10 +30,11 @@ export type Language = z.infer; export const isLanguage = (v: unknown): v is Language => zLanguage.safeParse(v).success; export const zSystemState = z.object({ - _version: z.literal(2), + _version: z.literal(3), shouldConfirmOnDelete: z.boolean(), shouldAntialiasProgressImage: z.boolean(), shouldConfirmOnNewSession: z.boolean(), + shouldProtectStarredImages: z.boolean(), language: zLanguage, shouldUseNSFWChecker: z.boolean(), shouldUseWatermarker: z.boolean(), diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 878973c34c9..e1e2f31ac84 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -479,6 +479,7 @@ export const { useDeleteBoardAndImagesMutation, useDeleteUncategorizedImagesMutation, useDeleteBoardMutation, + useDeleteImagesMutation, useStarImagesMutation, useUnstarImagesMutation, useBulkDownloadImagesMutation,