board UI updates: always show search for boards and images if a term is entered, clear search when view is toggled off

This commit is contained in:
Mary Hipp 2024-07-23 13:18:13 -04:00 committed by psychedelicious
parent 2172e4d292
commit b7383cc0e5
5 changed files with 123 additions and 103 deletions

View File

@ -374,6 +374,8 @@
"displayBoardSearch": "Display Board Search", "displayBoardSearch": "Display Board Search",
"displaySearch": "Display Search", "displaySearch": "Display Search",
"download": "Download", "download": "Download",
"exitBoardSearch": "Exit Board Search",
"exitSearch": "Exit Search",
"featuresWillReset": "If you delete this image, those features will immediately be reset.", "featuresWillReset": "If you delete this image, those features will immediately be reset.",
"galleryImageSize": "Image Size", "galleryImageSize": "Image Size",
"gallerySettings": "Gallery Settings", "gallerySettings": "Gallery Settings",

View File

@ -28,8 +28,10 @@ export const BoardsList = ({ isPrivate }: { isPrivate?: boolean }) => {
} }
return boards.filter((board) => { return boards.filter((board) => {
if (boardSearchText) { if (boardSearchText.length) {
return board.is_private === isPrivate && board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()); return (
board.is_private === !!isPrivate && board.board_name.toLowerCase().includes(boardSearchText.toLowerCase())
);
} else { } else {
return board.is_private === !!isPrivate; return board.is_private === !!isPrivate;
} }

View File

@ -0,0 +1,85 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Collapse, Flex, IconButton, Spacer, Tab, TabList, Tabs, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { galleryViewChanged, searchTermChanged } from 'features/gallery/store/gallerySlice';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { MdSearch, MdSearchOff } from 'react-icons/md';
import { COLLAPSE_STYLES } from './ImageGalleryContent';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination';
import { GallerySearch } from './ImageGrid/GallerySearch';
const BASE_STYLES: ChakraProps['sx'] = {
fontWeight: 'semibold',
fontSize: 'sm',
color: 'base.300',
};
const SELECTED_STYLES: ChakraProps['sx'] = {
borderColor: 'base.800',
borderBottomColor: 'base.900',
color: 'invokeBlue.300',
};
export const Gallery = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const galleryView = useAppSelector((s) => s.gallery.galleryView);
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
const searchDisclosure = useDisclosure({ defaultIsOpen: !!searchTerm.length });
const handleClickImages = useCallback(() => {
dispatch(galleryViewChanged('images'));
}, [dispatch]);
const handleClickAssets = useCallback(() => {
dispatch(galleryViewChanged('assets'));
}, [dispatch]);
const handleClickSearch = useCallback(() => {
if (searchTerm.length) {
dispatch(searchTermChanged(''));
searchDisclosure.onToggle();
} else {
searchDisclosure.onToggle();
}
}, [searchTerm, dispatch, searchDisclosure]);
return (
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full">
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
<TabList gap={2} fontSize="sm" borderColor="base.800">
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickImages} data-testid="images-tab">
{t('parameters.images')}
</Tab>
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
{t('gallery.assets')}
</Tab>
<Spacer />
<Box position="relative">
<IconButton
w="full"
h="full"
onClick={handleClickSearch}
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
aria-label={t('gallery.displaySearch')}
icon={searchTerm.length ? <MdSearchOff /> : <MdSearch />}
variant="link"
/>
</Box>
</TabList>
</Tabs>
<Box w="full">
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<GallerySearch />
</Box>
</Collapse>
</Box>
<GalleryImageGrid />
<GalleryPagination />
</Flex>
);
};

View File

@ -1,57 +1,28 @@
import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
import {
Box,
Collapse,
Divider,
Flex,
IconButton,
Spacer,
Tab,
TabList,
Tabs,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader'; import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { galleryViewChanged } from 'features/gallery/store/gallerySlice'; import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel'; import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiMagnifyingGlassBold } from 'react-icons/pi'; import { MdSearch, MdSearchOff } from 'react-icons/md';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper'; import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper';
import BoardsSearch from './Boards/BoardsList/BoardsSearch'; import BoardsSearch from './Boards/BoardsList/BoardsSearch';
import { Gallery } from './Gallery';
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover'; import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination';
import { GallerySearch } from './ImageGrid/GallerySearch';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 }; export const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
const BASE_STYLES: ChakraProps['sx'] = {
fontWeight: 'semibold',
fontSize: 'sm',
color: 'base.300',
};
const SELECTED_STYLES: ChakraProps['sx'] = {
borderColor: 'base.800',
borderBottomColor: 'base.900',
color: 'invokeBlue.300',
};
const ImageGalleryContent = () => { const ImageGalleryContent = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const galleryView = useAppSelector((s) => s.gallery.galleryView);
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText); const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const searchDisclosure = useDisclosure({ defaultIsOpen: false }); const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: false });
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null); const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const boardsListPanelOptions = useMemo<UsePanelOptions>( const boardsListPanelOptions = useMemo<UsePanelOptions>(
@ -67,13 +38,20 @@ const ImageGalleryContent = () => {
); );
const boardsListPanel = usePanel(boardsListPanelOptions); const boardsListPanel = usePanel(boardsListPanelOptions);
const handleClickImages = useCallback(() => { const handleClickBoardSearch = useCallback(() => {
dispatch(galleryViewChanged('images')); if (boardSearchText.length) {
}, [dispatch]); dispatch(boardSearchTextChanged(''));
boardSearchDisclosure.onToggle();
} else {
boardSearchDisclosure.onToggle();
}
}, [boardSearchText, dispatch, boardSearchDisclosure]);
const handleClickAssets = useCallback(() => { useEffect(() => {
dispatch(galleryViewChanged('assets')); if (boardSearchDisclosure.isOpen) {
}, [dispatch]); boardsListPanel.expand();
}
}, [boardSearchDisclosure, boardsListPanel]);
return ( return (
<Flex position="relative" flexDirection="column" h="full" w="full" pt={2}> <Flex position="relative" flexDirection="column" h="full" w="full" pt={2}>
@ -84,25 +62,17 @@ const ImageGalleryContent = () => {
<IconButton <IconButton
w="full" w="full"
h="full" h="full"
onClick={boardSearchDisclosure.onToggle} onClick={handleClickBoardSearch}
tooltip={`${t('gallery.displayBoardSearch')}`} tooltip={
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
}
aria-label={t('gallery.displayBoardSearch')} aria-label={t('gallery.displayBoardSearch')}
icon={<PiMagnifyingGlassBold />} icon={boardSearchText.length ? <MdSearchOff /> : <MdSearch />}
variant="link" variant="link"
/> />
{boardSearchText && (
<Box
position="absolute"
w={2}
h={2}
bg="invokeBlue.300"
borderRadius="full"
insetBlockStart={2}
insetInlineEnd={2}
/>
)}
</Box> </Box>
</Flex> </Flex>
<PanelGroup ref={panelGroupRef} direction="vertical"> <PanelGroup ref={panelGroupRef} direction="vertical">
<Panel <Panel
id="boards-list-panel" id="boards-list-panel"
@ -127,50 +97,7 @@ const ImageGalleryContent = () => {
onDoubleClick={boardsListPanel.onDoubleClickHandle} onDoubleClick={boardsListPanel.onDoubleClickHandle}
/> />
<Panel id="gallery-wrapper-panel" minSize={20}> <Panel id="gallery-wrapper-panel" minSize={20}>
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full"> <Gallery />
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
<TabList gap={2} fontSize="sm" borderColor="base.800">
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickImages} data-testid="images-tab">
{t('parameters.images')}
</Tab>
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
{t('gallery.assets')}
</Tab>
<Spacer />
<Box position="relative">
<IconButton
w="full"
h="full"
onClick={searchDisclosure.onToggle}
tooltip={`${t('gallery.displaySearch')}`}
aria-label={t('gallery.displaySearch')}
icon={<PiMagnifyingGlassBold />}
variant="link"
/>
{searchTerm && (
<Box
position="absolute"
w={2}
h={2}
bg="invokeBlue.300"
borderRadius="full"
insetBlockStart={2}
insetInlineEnd={2}
/>
)}
</Box>
</TabList>
</Tabs>
<Box w="full">
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
<Box w="full" pt={2}>
<GallerySearch />
</Box>
</Collapse>
</Box>
<GalleryImageGrid />
<GalleryPagination />
</Flex>
</Panel> </Panel>
</PanelGroup> </PanelGroup>
</Flex> </Flex>

View File

@ -4,7 +4,7 @@ import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelecto
import { searchTermChanged } from 'features/gallery/store/gallerySlice'; import { searchTermChanged } from 'features/gallery/store/gallerySlice';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi'; import { PiXBold } from 'react-icons/pi';
import { useListImagesQuery } from 'services/api/endpoints/images'; import { useListImagesQuery } from 'services/api/endpoints/images';
@ -37,6 +37,10 @@ export const GallerySearch = () => {
dispatch(searchTermChanged('')); dispatch(searchTermChanged(''));
}, [dispatch]); }, [dispatch]);
useEffect(() => {
setSearchTermInput(searchTerm);
}, [searchTerm]);
return ( return (
<InputGroup> <InputGroup>
<Input <Input