import _ from "lodash"; import React, { useState } from "react"; import { Button, Callout, Card, H5, Popover, Tooltip } from "@blueprintjs/core"; import styles from "./paginated-results.module.css"; import { SKELETON } from "@blueprintjs/core/lib/cjs/common/classes"; import { useStateLink } from "@hookstate/core"; import { v4 as uuidv4 } from "uuid"; import { useInfiniteQuery } from "react-query"; import FocusableCard from "../card"; import Highlighter from "../highlighter"; const numberFormatter = new Intl.NumberFormat(); const loader = ( <FocusableCard> <div> <p className={SKELETON}>Loading...</p> <p className={SKELETON} style={{ height: "3rem" }}> Loading... </p> </div> </FocusableCard> ); function renderResults({ key, component, item, state, itemDetailRenderer } = {}) { const focusKey = uuidv4(); return ( <FocusableCard data-focus-key={focusKey} key={key} interactive={!!item && itemDetailRenderer} onClick={ item && itemDetailRenderer ? () => { // Ugly workaround: We cannot pass a reference to a view with hookState so // pass it though the global window object instead :/ window.detailView = itemDetailRenderer; item.__focusKey = focusKey; state.nested.selectedItem.set(item); } : undefined } className={styles.resultItem} > {component ? ( <Highlighter text={state.nested.input.get()}>{component}</Highlighter> ) : ( "Unable to render results." )} </FocusableCard> ); } export function PaginatedResults({ searchViewState, logo, configuration, queryKey, fetcher, getFetchMore, renderPages = _.noop, getTotal = _.noop, filters, itemDetailRenderer, globalError, }) { const state = useStateLink(searchViewState); const [listRef, setListRef] = useState(null); const { status, data, isFetchingMore, fetchMore, canFetchMore, error, isFetching, } = useInfiniteQuery(globalError ? null : queryKey, fetcher, { getFetchMore }); const total = getTotal(data); const { name, pageSize } = configuration.get(); const pages = status === "success" ? renderPages({ pages: data, error }) : []; const isEmpty = _.isEmpty(pages); return ( <div className={styles.results}> <div className={styles.resultsHeader}> {logo && <img className={styles.moduleLogo} src={logo} alt={`${name} logo`} />} {!error && total > 0 ? ( <> <Tooltip content={numberFormatter.format(total) + " " + (total === 1 ? "result" : "results")} > <H5 className={styles.moduleTitle}>{name}</H5> </Tooltip> <p className={styles.resultTotal}> ({numberFormatter.format(total)} {total === 1 ? "result" : "results"}) </p> </> ) : ( <H5 className={styles.moduleTitle}>{name}</H5> )} {filters && ( <> <div className={styles.moduleFilters}>{filters}</div> <Popover className={styles.moduleFiltersMore} hasBackdrop position={"left"} target={ <Tooltip content="Filters"> <Button icon="more" small minimal style={{ margin: 3 }} /> </Tooltip> } content={<div className={styles.moduleFiltersMoreContainer}>{filters}</div>} /> </> )} </div> <div className={styles.resultList} ref={setListRef}> {globalError ? ( <Callout intent="danger" className={styles.resultItem}> {globalError} </Callout> ) : status === "loading" ? null : status === "error" ? ( <Callout intent="danger" className={styles.resultItem}> {error.message} </Callout> ) : _.isEmpty(pages) ? ( <Card className={styles.resultItem}>No results.</Card> ) : ( pages.map(({ key, component, item } = {}) => renderResults({ key, component, item, state, itemDetailRenderer }) ) )} {(!globalError && isFetching && isEmpty) || isFetchingMore ? Array(isFetching ? pageSize : 1) .fill(0) .map((id, index) => <React.Fragment key={index}>{loader}</React.Fragment>) : null} </div> {canFetchMore && !isFetchingMore && ( <div> <Button className="focusable" minimal intent="primary" rightIcon="arrow-right" onClick={() => { const nodes = listRef.querySelectorAll(".focusable"); nodes[nodes.length - 1].focus(); fetchMore(); }} > Load More </Button> </div> )} </div> ); }