import * as React from 'react'; import { Button, IconName, InputGroup, NonIdealState, Popover, Spinner } from '@blueprintjs/core'; import { AutoSizer, Grid } from 'react-virtualized'; import { DataItem, SearchQuery, SearchQuerySortDirection } from '../../../types'; import { PageHeader } from '../../common/PageHeader'; import { useEffect, useRef, useState } from 'react'; import { useDataSearch } from '../../../datasource/useDataSearch'; import { searchViewCellDimensions } from './searchViewCellDimensions'; import { SearchViewCard } from './SearchViewCard'; import { PageContainer } from '../../common/PageContainer'; import { SearchInput } from './SearchInput'; import { SearchSortingMenu } from './SearchSortingMenu'; import { useDataItemPreviews } from '../../../datasource/useDataItemPreviews'; import { LoadingSearchViewCard } from './LoadingSearchViewCard'; import { SearchBar } from '../../searchbar/SearchBar'; import { DataItemContextMenu } from '../../menus/DataItemContextMenu'; import { Bp3MenuRenderer } from '../../menus/Bp3MenuRenderer'; import { useDataItem } from '../../../datasource/useDataItem'; import { useMainContentContext } from '../context'; import { useDataInterface } from '../../../datasource/DataInterfaceContext'; import { useOverlaySearch } from '../../overlaySearch/OverlaySearchProvider'; export const SearchView: React.FC<{ title: string; titleSubtext?: string; icon: IconName; iconColor?: string; hiddenSearch: SearchQuery; defaultSearch: SearchQuery; onClickItem?: (item: DataItem) => void; rightContent?: React.ReactNode; }> = props => { const mainContent = useMainContentContext(); const dataInterface = useDataInterface(); const overlaySearch = useOverlaySearch(); const [hiddenSearch, setHiddenSearch] = useState(props.hiddenSearch); const [userSearch, setUserSearch] = useState(props.defaultSearch); // const [searchQuery, setSearchQuery] = useState<SearchQuery>({ ...props.hiddenSearch, ...props.defaultSearch }); const searchQuery = { ...hiddenSearch, ...userSearch }; const { items, nextPageAvailable, fetchNextPage, isFetching } = useDataSearch( Object.keys(searchQuery).length === 0 ? { all: true } : searchQuery, 200 ); const parent = useDataItem(hiddenSearch.parents?.[0]); const previews = useDataItemPreviews(items); // TODO currently, all previews are loaded at once. Use Grid.onSectionRendered() to only load previews when they enter the viewport useEffect(() => setHiddenSearch(q => ({ ...q, ...props.hiddenSearch })), [props.hiddenSearch]); useEffect(() => setUserSearch(q => ({ ...q, ...props.defaultSearch })), [props.defaultSearch]); return ( <PageContainer header={ <PageHeader title={props.title} titleSubtext={props.titleSubtext} icon={props.icon} iconColor={props.iconColor} lowerContent={ <SearchBar onChangeSearchQuery={setUserSearch}> <SearchInput /> </SearchBar> } rightContent={ <> {props.rightContent}{' '} <Popover content={<SearchSortingMenu searchQuery={hiddenSearch} onChange={setHiddenSearch} />}> <Button icon={searchQuery.sortDirection === SearchQuerySortDirection.Ascending ? 'sort-asc' : 'sort-desc'} outlined > Sort Items </Button> </Popover> {parent && ( <> {' '} <Popover interactionKind={'click'} position={'bottom'} captureDismiss={true} content={ <DataItemContextMenu item={parent} renderer={Bp3MenuRenderer} mainContent={mainContent} dataInterface={dataInterface} overlaySearch={overlaySearch} /> } > <Button outlined rightIcon={'chevron-down'}> More </Button> </Popover> </> )} </> } /> } > <AutoSizer> {({ width, height }) => { if (width <= searchViewCellDimensions.cellWidth) return null; const rowCount = Math.ceil(items.length / Math.floor(width / searchViewCellDimensions.cellWidth)); const columnCount = Math.floor(width / searchViewCellDimensions.cellWidth); return ( <Grid cellRenderer={cellProps => { const itemId = cellProps.rowIndex * Math.floor(width / searchViewCellDimensions.cellWidth) + cellProps.columnIndex; const additionalLeftMargin = (width - columnCount * searchViewCellDimensions.cellWidth) / 2; if (items.length === 0) { return null; // Provoke non ideal state } if (itemId >= items.length) { if (!nextPageAvailable) { return null; } fetchNextPage(); return ( <LoadingSearchViewCard key={cellProps.key} additionalLeftMargin={additionalLeftMargin} containerStyle={cellProps.style} /> ); } const item = items[itemId]; return ( <SearchViewCard cellProps={cellProps} dataItem={item} additionalLeftMargin={additionalLeftMargin} onClick={props.onClickItem ? () => props.onClickItem?.(item) : undefined} preview={previews[item.id]} /> ); }} columnWidth={searchViewCellDimensions.cellWidth} columnCount={columnCount} noContentRenderer={() => isFetching ? ( <NonIdealState icon={<Spinner />} title="Loading items..." /> ) : ( <NonIdealState icon={'warning-sign'} title="No items found" /> ) } overscanColumnCount={0} overscanRowCount={6} rowHeight={searchViewCellDimensions.cellHeight} rowCount={rowCount + (nextPageAvailable ? 12 : 0)} onSectionRendered={section => {}} height={height} width={width} /> ); }} </AutoSizer> </PageContainer> ); };