123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- import {Fragment, memo, useEffect, useMemo} from 'react';
- import styled from '@emotion/styled';
- import AutoComplete from 'sentry/components/autoComplete';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {Result} from './sources/types';
- import SearchResult from './searchResult';
- import SearchResultWrapper from './searchResultWrapper';
- type AutoCompleteOpts = Parameters<AutoComplete<Result['item']>['props']['children']>[0];
- interface RenderItemProps {
- highlighted: boolean;
- index: number;
- item: Result['item'];
- itemProps: ReturnType<AutoCompleteOpts['getItemProps']>;
- matches: Result['matches'];
- }
- type RenderItem = (props: RenderItemProps) => React.ReactNode;
- type Props = {
- getItemProps: AutoCompleteOpts['getItemProps'];
- hasAnyResults: boolean;
- highlightedIndex: number;
- isLoading: boolean;
- registerItemCount: AutoCompleteOpts['registerItemCount'];
- registerVisibleItem: AutoCompleteOpts['registerVisibleItem'];
- resultFooter: React.ReactNode;
- results: Result[];
- dropdownClassName?: string;
- maxResults?: number;
- renderItem?: RenderItem;
- };
- function defaultItemRenderer({item, highlighted, itemProps, matches}: RenderItemProps) {
- return (
- <SearchResultWrapper highlighted={highlighted} {...itemProps}>
- <SearchResult highlighted={highlighted} item={item} matches={matches} />
- </SearchResultWrapper>
- );
- }
- function List({
- dropdownClassName,
- isLoading,
- hasAnyResults,
- results,
- maxResults,
- getItemProps,
- highlightedIndex,
- resultFooter,
- registerItemCount,
- registerVisibleItem,
- renderItem = defaultItemRenderer,
- }: Props) {
- const resultList = results.slice(0, maxResults);
- useEffect(
- () => registerItemCount(resultList.length),
- [registerItemCount, resultList.length]
- );
- return (
- <DropdownBox className={dropdownClassName}>
- {isLoading ? (
- <LoadingWrapper>
- <LoadingIndicator mini hideMessage relative />
- </LoadingWrapper>
- ) : !hasAnyResults ? (
- <EmptyItem>{t('No results found')}</EmptyItem>
- ) : (
- resultList.map((result, index) => {
- const {item, matches, refIndex} = result;
- const highlighted = index === highlightedIndex;
- const resultProps = {
- renderItem,
- registerVisibleItem,
- getItemProps,
- highlighted,
- index,
- item,
- matches,
- };
- return <ResultRow key={`${index}-${refIndex}`} {...resultProps} />;
- })
- )}
- {!isLoading && resultFooter ? <ResultFooter>{resultFooter}</ResultFooter> : null}
- </DropdownBox>
- );
- }
- type SearchItemProps = {
- getItemProps: Props['getItemProps'];
- highlighted: boolean;
- index: number;
- item: Result['item'];
- matches: Result['matches'];
- registerVisibleItem: Props['registerVisibleItem'];
- renderItem: RenderItem;
- };
- // XXX(epurkhiser): We memoize the ResultRow component since there will be many
- // of them, we do not want them re-rendering every time we change the
- // highlightedIndex in the parent List.
- /**
- * Search item is used to call `registerVisibleItem` any time the item changes
- */
- const ResultRow = memo(
- ({
- renderItem,
- registerVisibleItem,
- getItemProps,
- ...renderItemProps
- }: SearchItemProps) => {
- const {item, index} = renderItemProps;
- useEffect(() => registerVisibleItem(index, item), [registerVisibleItem, item]);
- const itemProps = useMemo(
- () => getItemProps({item, index}),
- [getItemProps, item, index]
- );
- return <Fragment>{renderItem({itemProps, ...renderItemProps})}</Fragment>;
- }
- );
- export default List;
- const DropdownBox = styled('div')`
- background: ${p => p.theme.background};
- border: 1px solid ${p => p.theme.border};
- box-shadow: ${p => p.theme.dropShadowHeavy};
- position: absolute;
- top: 36px;
- right: 0;
- width: 400px;
- border-radius: 5px;
- overflow: auto;
- max-height: 60vh;
- `;
- const ResultFooter = styled('div')`
- position: sticky;
- bottom: 0;
- left: 0;
- right: 0;
- `;
- const EmptyItem = styled(SearchResultWrapper)`
- text-align: center;
- padding: 16px;
- opacity: 0.5;
- `;
- const LoadingWrapper = styled('div')`
- display: flex;
- justify-content: center;
- align-items: center;
- padding: ${space(1)};
- `;
|