From 6972cd708dd2855d00555e5e1ec9afb823aaeec5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:34:45 +1000 Subject: [PATCH] feat(ui): delete confirmation for videos --- invokeai/frontend/web/public/locales/en.json | 3 + .../app/components/GlobalModalIsolator.tsx | 2 + .../web/src/common/hooks/useGlobalHotkeys.ts | 11 +- .../hooks/use-delete-video.ts | 28 +++++ .../components/DeleteVideoButton.tsx | 36 ++++++ .../components/DeleteVideoModal.tsx | 43 +++++++ .../features/deleteVideoModal/store/state.ts | 111 ++++++++++++++++++ ...ete.tsx => ContextMenuItemDeleteImage.tsx} | 11 +- .../MenuItems/ContextMenuItemDeleteVideo.tsx | 35 ++++++ .../ContextMenu/SingleSelectionMenuItems.tsx | 4 +- .../SingleSelectionVideoMenuItems.tsx | 4 +- .../ImageViewer/CurrentVideoButtons.tsx | 35 +++--- 12 files changed, 291 insertions(+), 32 deletions(-) create mode 100644 invokeai/frontend/web/src/features/deleteImageModal/hooks/use-delete-video.ts create mode 100644 invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoButton.tsx create mode 100644 invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx create mode 100644 invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts rename invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/{ContextMenuItemDelete.tsx => ContextMenuItemDeleteImage.tsx} (69%) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b90b137100..3b7a630c90 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -365,6 +365,9 @@ "deleteImage_one": "Delete Image", "deleteImage_other": "Delete {{count}} Images", "deleteImagePermanent": "Deleted images cannot be restored.", + "deleteVideo_one": "Delete Video", + "deleteVideo_other": "Delete {{count}} Videos", + "deleteVideoPermanent": "Deleted videos cannot be restored.", "displayBoardSearch": "Board Search", "displaySearch": "Image Search", "download": "Download", diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx index 61a9aebf28..b6e64dc86c 100644 --- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -3,6 +3,7 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal'; +import { DeleteVideoModal } from 'features/deleteVideoModal/components/DeleteVideoModal'; import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal'; @@ -32,6 +33,7 @@ export const GlobalModalIsolator = memo(() => { return ( <> + diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 7bdd062625..be12e75487 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -1,5 +1,6 @@ import { useAppStore } from 'app/store/storeHooks'; import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state'; +import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state'; import { selectSelection } from 'features/gallery/store/gallerySelectors'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; @@ -123,6 +124,8 @@ export const useGlobalHotkeys = () => { }); const deleteImageModalApi = useDeleteImageModalApi(); + const deleteVideoModalApi = useDeleteVideoModalApi(); + useRegisteredHotkeys({ id: 'deleteSelection', category: 'gallery', @@ -135,7 +138,13 @@ export const useGlobalHotkeys = () => { if (!selection.length) { return; } - deleteImageModalApi.delete(selection.map((s) => s.id)); + if (selection.every(({ type }) => type === 'image')) { + deleteImageModalApi.delete(selection.map((s) => s.id)); + } else if (selection.every(({ type }) => type === 'video')) { + deleteVideoModalApi.delete(selection.map((s) => s.id)); + } else { + // no-op, we expect selections to always be only images or only video + } }, dependencies: [getState, deleteImageModalApi], }); diff --git a/invokeai/frontend/web/src/features/deleteImageModal/hooks/use-delete-video.ts b/invokeai/frontend/web/src/features/deleteImageModal/hooks/use-delete-video.ts new file mode 100644 index 0000000000..b14cd70ebe --- /dev/null +++ b/invokeai/frontend/web/src/features/deleteImageModal/hooks/use-delete-video.ts @@ -0,0 +1,28 @@ +import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state'; +import { useCallback, useMemo } from 'react'; +import type { VideoDTO } from 'services/api/types'; + +export const useDeleteVideo = (videoDTO?: VideoDTO | null) => { + const deleteImageModal = useDeleteVideoModalApi(); + + const isEnabled = useMemo(() => { + if (!videoDTO) { + return; + } + return true; + }, [videoDTO]); + const _delete = useCallback(() => { + if (!videoDTO) { + return; + } + if (!isEnabled) { + return; + } + deleteImageModal.delete([videoDTO.video_id]); + }, [deleteImageModal, videoDTO, isEnabled]); + + return { + delete: _delete, + isEnabled, + }; +}; diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoButton.tsx b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoButton.tsx new file mode 100644 index 0000000000..9e56bfba7d --- /dev/null +++ b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoButton.tsx @@ -0,0 +1,36 @@ +import type { IconButtonProps } from '@invoke-ai/ui-library'; +import { IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectSelectionCount } from 'features/gallery/store/gallerySelectors'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import { $isConnected } from 'services/events/stores'; + +type Props = Omit & { + onClick: () => void; +}; + +export const DeleteVideoButton = memo((props: Props) => { + const { onClick, isDisabled } = props; + const { t } = useTranslation(); + const isConnected = useStore($isConnected); + const count = useAppSelector(selectSelectionCount); + const labelMessage: string = `${t('gallery.deleteVideo', { count })} (Del)`; + + return ( + } + tooltip={labelMessage} + aria-label={labelMessage} + isDisabled={isDisabled || !isConnected} + colorScheme="error" + variant="link" + alignSelf="stretch" + /> + ); +}); + +DeleteVideoButton.displayName = 'DeleteVideoButton'; diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx new file mode 100644 index 0000000000..662e7e6f77 --- /dev/null +++ b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx @@ -0,0 +1,43 @@ +import { ConfirmationAlertDialog, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useDeleteVideoModalApi, useDeleteVideoModalState } from 'features/deleteVideoModal/store/state'; +import { selectSystemShouldConfirmOnDelete, setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const DeleteVideoModal = memo(() => { + const state = useDeleteVideoModalState(); + const api = useDeleteVideoModalApi(); + const { dispatch } = useAppStore(); + const { t } = useTranslation(); + const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete); + + const handleChangeShouldConfirmOnDelete = useCallback( + (e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)), + [dispatch] + ); + + return ( + + + {t('gallery.deleteVideoPermanent')} + {t('common.areYouSure')} + + {t('common.dontAskMeAgain')} + + + + + ); +}); +DeleteVideoModal.displayName = 'DeleteVideoModal'; diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts new file mode 100644 index 0000000000..4e7580ce30 --- /dev/null +++ b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts @@ -0,0 +1,111 @@ +import { useStore } from '@nanostores/react'; +import type { AppStore } from 'app/store/store'; +import { useAppStore } from 'app/store/storeHooks'; +import { intersection } from 'es-toolkit/compat'; +import { selectGetVideoIdsQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { itemSelected } from 'features/gallery/store/gallerySlice'; +import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice'; +import { atom } from 'nanostores'; +import { useMemo } from 'react'; +import { videosApi } from 'services/api/endpoints/videos'; + +// Implements an awaitable modal dialog for deleting images + +type DeleteVideosModalState = { + video_ids: string[]; + isOpen: boolean; + resolve?: () => void; + reject?: (reason?: string) => void; +}; + +const getInitialState = (): DeleteVideosModalState => ({ + video_ids: [], + isOpen: false, +}); + +const $deleteVideosModalState = atom(getInitialState()); + +const deleteVideosWithDialog = async (video_ids: string[], store: AppStore): Promise => { + const { getState } = store; + const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState()); + + if (!shouldConfirmOnDelete) { + // If we don't need to confirm and the resources are not in use, delete them directly + await handleDeletions(video_ids, store); + return; + } + + return new Promise((resolve, reject) => { + $deleteVideosModalState.set({ + video_ids, + isOpen: true, + resolve, + reject, + }); + }); +}; + +const handleDeletions = async (video_ids: string[], store: AppStore) => { + try { + const { dispatch, getState } = store; + const state = getState(); + const { data } = videosApi.endpoints.getVideoIds.select(selectGetVideoIdsQueryArgs(state))(state); + const index = data?.video_ids.findIndex((id) => id === video_ids[0]); + const { deleted_videos } = await dispatch( + videosApi.endpoints.deleteVideos.initiate({ video_ids }, { track: false }) + ).unwrap(); + + const newVideoIds = data?.video_ids.filter((id) => !deleted_videos.includes(id)) || []; + const newSelectedVideoId = newVideoIds[index ?? 0] || null; + + if ( + intersection( + state.gallery.selection.map((s) => s.id), + video_ids + ).length > 0 && + newSelectedVideoId + ) { + // Some selected images were deleted, clear selection + dispatch(itemSelected({ type: 'video', id: newSelectedVideoId })); + } + } catch { + // no-op + } +}; + +const confirmDeletion = async (store: AppStore) => { + const state = $deleteVideosModalState.get(); + await handleDeletions(state.video_ids, store); + state.resolve?.(); + closeSilently(); +}; + +const cancelDeletion = () => { + const state = $deleteVideosModalState.get(); + state.reject?.('User canceled'); + closeSilently(); +}; + +const closeSilently = () => { + $deleteVideosModalState.set(getInitialState()); +}; + +export const useDeleteVideoModalState = () => { + const state = useStore($deleteVideosModalState); + return state; +}; + +export const useDeleteVideoModalApi = () => { + const store = useAppStore(); + const api = useMemo( + () => ({ + delete: (video_ids: string[]) => deleteVideosWithDialog(video_ids, store), + confirm: () => confirmDeletion(store), + cancel: cancelDeletion, + close: closeSilently, + }), + [store] + ); + + return api; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDelete.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteImage.tsx similarity index 69% rename from invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDelete.tsx rename to invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteImage.tsx index ac42c65124..dd0fa1908f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDelete.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteImage.tsx @@ -4,27 +4,22 @@ import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -import { useDeleteVideosMutation } from 'services/api/endpoints/videos'; import { isImageDTO } from 'services/api/types'; -export const ContextMenuItemDelete = memo(() => { +export const ContextMenuItemDeleteImage = memo(() => { const { t } = useTranslation(); const deleteImageModal = useDeleteImageModalApi(); - const [deleteVideos] = useDeleteVideosMutation(); const itemDTO = useItemDTOContext(); const onClick = useCallback(async () => { try { if (isImageDTO(itemDTO)) { await deleteImageModal.delete([itemDTO.image_name]); - } else { - // TODO: Add confirm on delete and video usage functionality - await deleteVideos({ video_ids: [itemDTO.video_id] }); } } catch { // noop; } - }, [deleteImageModal, deleteVideos, itemDTO]); + }, [deleteImageModal, itemDTO]); return ( { ); }); -ContextMenuItemDelete.displayName = 'ContextMenuItemDelete'; +ContextMenuItemDeleteImage.displayName = 'ContextMenuItemDeleteImage'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx new file mode 100644 index 0000000000..64e5d100a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx @@ -0,0 +1,35 @@ +import { IconMenuItem } from 'common/components/IconMenuItem'; +import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state'; +import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import { isVideoDTO } from 'services/api/types'; + +export const ContextMenuItemDeleteVideo = memo(() => { + const { t } = useTranslation(); + const deleteVideoModal = useDeleteVideoModalApi(); + const itemDTO = useItemDTOContext(); + + const onClick = useCallback(async () => { + try { + if (isVideoDTO(itemDTO)) { + await deleteVideoModal.delete([itemDTO.video_id]); + } + } catch { + // noop; + } + }, [deleteVideoModal, itemDTO]); + + return ( + } + onClickCapture={onClick} + aria-label={t('gallery.deleteVideo', { count: 1 })} + tooltip={t('gallery.deleteVideo', { count: 1 })} + isDestructive + /> + ); +}); + +ContextMenuItemDeleteVideo.displayName = 'ContextMenuItemDeleteVideo'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx index 5209e992fb..13c68deafd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx @@ -3,7 +3,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IconMenuItemGroup } from 'common/components/IconMenuItem'; import { ContextMenuItemChangeBoard } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoard'; import { ContextMenuItemCopy } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemCopy'; -import { ContextMenuItemDelete } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDelete'; import { ContextMenuItemDownload } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownload'; import { ContextMenuItemLoadWorkflow } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLoadWorkflow'; import { ContextMenuItemLocateInGalery } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery'; @@ -23,6 +22,7 @@ import { ItemDTOContextProvider } from 'features/gallery/contexts/ItemDTOContext import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { ImageDTO } from 'services/api/types'; +import { ContextMenuItemDeleteImage } from './MenuItems/ContextMenuItemDeleteImage'; import { ContextMenuItemMetadataRecallActionsUpscaleTab } from './MenuItems/ContextMenuItemMetadataRecallActionsUpscaleTab'; type SingleSelectionMenuItemsProps = { @@ -40,7 +40,7 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) = - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems.tsx index cd9b2421ee..7d529c41de 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems.tsx @@ -2,7 +2,6 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { IconMenuItemGroup } from 'common/components/IconMenuItem'; import { ContextMenuItemChangeBoard } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoard'; import { ContextMenuItemCopy } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemCopy'; -import { ContextMenuItemDelete } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDelete'; import { ContextMenuItemDownload } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownload'; import { ContextMenuItemOpenInNewTab } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab'; import { ContextMenuItemOpenInViewer } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInViewer'; @@ -10,6 +9,7 @@ import { ContextMenuItemSelectForCompare } from 'features/gallery/components/Con import { ItemDTOContextProvider } from 'features/gallery/contexts/ItemDTOContext'; import type { VideoDTO } from 'services/api/types'; +import { ContextMenuItemDeleteVideo } from './MenuItems/ContextMenuItemDeleteVideo'; import { ContextMenuItemStarUnstar } from './MenuItems/ContextMenuItemStarUnstar'; type SingleSelectionVideoMenuItemsProps = { @@ -25,7 +25,7 @@ const SingleSelectionVideoMenuItems = ({ videoDTO }: SingleSelectionVideoMenuIte - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx index c8db112f55..d68fc22bd8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx @@ -1,6 +1,7 @@ import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; +import { useDeleteVideo } from 'features/deleteImageModal/hooks/use-delete-video'; +import { DeleteVideoButton } from 'features/deleteVideoModal/components/DeleteVideoButton'; import SingleSelectionVideoMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems'; import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { navigationApi } from 'features/ui/layouts/navigation-api'; @@ -12,7 +13,6 @@ import { memo, useCallback, useState } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { PiCameraBold, PiCrosshairBold, PiDotsThreeOutlineFill, PiSpinnerBold } from 'react-icons/pi'; -import { useDeleteVideosMutation } from 'services/api/endpoints/videos'; import type { VideoDTO } from 'services/api/types'; export const CurrentVideoButtons = memo(({ videoDTO }: { videoDTO: VideoDTO }) => { @@ -21,7 +21,7 @@ export const CurrentVideoButtons = memo(({ videoDTO }: { videoDTO: VideoDTO }) = const dispatch = useAppDispatch(); const activeTab = useAppSelector(selectActiveTab); const galleryPanel = useGalleryPanel(activeTab); - const [deleteVideos] = useDeleteVideosMutation(); + const deleteVideo = useDeleteVideo(videoDTO); const captureVideoFrame = useCaptureVideoFrame(); const { videoRef } = useVideoViewerContext(); @@ -43,10 +43,6 @@ export const CurrentVideoButtons = memo(({ videoDTO }: { videoDTO: VideoDTO }) = }); }, [dispatch, galleryPanel, videoDTO]); - const handleDelete = useCallback(() => { - deleteVideos({ video_ids: [videoDTO.video_id] }); - }, [deleteVideos, videoDTO]); - const onClickSaveFrame = useCallback(async () => { setCapturing(true); await captureVideoFrame(videoRef.current); @@ -98,20 +94,21 @@ export const CurrentVideoButtons = memo(({ videoDTO }: { videoDTO: VideoDTO }) = {doesTabHaveGallery && ( - } - aria-label={t('boards.locateInGalery')} - tooltip={t('boards.locateInGalery')} - onClick={locateInGallery} - variant="link" - size="sm" - alignSelf="stretch" - /> + <> + } + aria-label={t('boards.locateInGalery')} + tooltip={t('boards.locateInGalery')} + onClick={locateInGallery} + variant="link" + size="sm" + alignSelf="stretch" + /> + + )} - - - + ); });