import React, { FC, PropsWithChildren, ReactElement, ReactNode, useCallback, } from 'react' import ReactTable, { TableProps } from 'react-table' import { basePrimaryStyles, baseSecondaryStyles, } from '@monorail/helpers/baseStyles' import { BorderRadius } from '@monorail/helpers/borderRadius' import { Colors, getColor } from '@monorail/helpers/color' import { flexFlow } from '@monorail/helpers/flex' import styled, { css, ThemeProvider } from '@monorail/helpers/styled-components' import { ThemeColors } from '@monorail/helpers/theme' import { all } from '@monorail/sharedHelpers/fp-ts-ext/Array' import { assertNever, isNil, isNotNil, } from '@monorail/sharedHelpers/typeGuards' import { StyledButton } from '@monorail/visualComponents/buttons/Button' import { ButtonsBarMode, ButtonSize, } from '@monorail/visualComponents/buttons/buttonTypes' import { IconButton } from '@monorail/visualComponents/buttons/IconButton' import { ButtonsBar } from '@monorail/visualComponents/buttonsBar/ButtonsBar' import { BBCardGrid } from '@monorail/visualComponents/cards/Cards' import { ComponentPropsGetterR, NoDataComponentVertical, ResizerComponent, TableColumns, TBodyComponent, ThComponent, ThComponentProps, TheadComponent, TheadComponentContainer, TheadComponentProps, TrGroupComponent, useSort, } from '@monorail/visualComponents/dataTable/ReactTable' import { DebouncedSearch } from '@monorail/visualComponents/inputs/DebouncedSearch' import { CompareSearchType, SearchController, } from '@monorail/visualComponents/inputs/SearchController' import { CollectionPaginationComponent } from './CollectionPaginationComponent' const CollectionContainer = styled.div` ${flexFlow('row')}; background: ${getColor(Colors.White)}; flex: 1; overflow: hidden; ` const ControlsContainer = styled.div( ({ cardViewWithoutFilters }: { cardViewWithoutFilters: boolean }) => css` ${flexFlow('row')}; align-items: center; background: ${getColor(Colors.Grey99)}; height: 40px; padding: 4px 32px 0 30px; /* Button Bar has a 2px gap that we are making up here. */ ${cardViewWithoutFilters ? `border-bottom: 1px solid ${getColor(Colors.Grey90)};` : null} `, ) export enum CollectionView { Table = 'table', Card = 'card', } export type SearchFilterType = (params: { item: T compareSearch: CompareSearchType value: string }) => boolean type ReactTableComponentProps = { children: ReactElement item?: T style?: { [key: string]: number | string } } type ReactTableComponent = ( props: ReactTableComponentProps, ) => ReactElement> type SearchFilter = { searchFilter: SearchFilterType } type SearchInput = { searchInput: ReactNode } export type CollectionProps = { // required collectionView: CollectionView columns: TableColumns data: TableProps['data'] setCollectionView: (collectionView: CollectionView) => void // optional cardRender?: (item: T) => ReactElement filters?: Array isLoading?: boolean NoDataComponent?: () => ReactElement PaginationComponent?: () => ReactElement pageSize?: number showPagination?: boolean pivotBy?: Array } & (SearchFilter | SearchInput) const PAGE_SIZE = 20 export const Collection = ( props: CollectionProps, ): ReactElement> => { const { cardRender, collectionView, columns, data, isLoading = false, pivotBy, setCollectionView, NoDataComponent = () => , PaginationComponent = CollectionPaginationComponent, pageSize = PAGE_SIZE, showPagination, } = props const [sorted, onSortedChange] = useSort() const getReactTableComponentProps: ComponentPropsGetterR = useCallback( (_finalState, rowInfo) => { if (!isNil(rowInfo)) { return { item: rowInfo.original, } } return }, [], ) const getTrComponent: ReactTableComponent = useCallback( ({ item, children }) => { if (!isNil(item)) { switch (collectionView) { case CollectionView.Card: return isNotNil(cardRender) ? cardRender(item) : children case CollectionView.Table: return children default: assertNever(collectionView) return children } } return children }, [cardRender, collectionView], ) const getTbodyComponent: ReactTableComponent = useCallback( ({ children, ...domProps }) => { switch (collectionView) { case CollectionView.Card: return {children} case CollectionView.Table: return {children} default: assertNever(collectionView) return <> } }, [collectionView], ) const getTrGroupComponent: ReactTableComponent = useCallback( ({ item, children, ...domProps }) => { if ( isNotNil(item) && collectionView === CollectionView.Card && isNotNil(cardRender) ) { return cardRender(item) } return {children} }, [collectionView, cardRender], ) const theadOnTableViewOnly = all( columns, column => column.filterable === false && column.sortable === false, ) const renderCollection = useCallback( ({ passedSearchInput, passedData, }: { passedSearchInput: ReactNode passedData: TableProps['data'] }) => ( <> {isNotNil(cardRender) && ( setCollectionView(CollectionView.Table)} icon="view_headline" /> setCollectionView(CollectionView.Card)} icon="view_module" /> )} {props.filters} {passedSearchInput} ({ ...theme, size: { ...theme.size, table: { margin: 32 } }, })} > css={collectionView === CollectionView.Card ? theadOverrides : ''} sorted={sorted} onSortedChange={onSortedChange} columns={columns} data={passedData} getTrGroupProps={getReactTableComponentProps} getTrProps={getReactTableComponentProps} loading={isLoading} pageSize={pageSize} pivotBy={pivotBy} TbodyComponent={getTbodyComponent} TrComponent={getTrComponent} TrGroupComponent={getTrGroupComponent} showPagination={showPagination || passedData.length > pageSize} PaginationComponent={PaginationComponent} NoDataComponent={NoDataComponent} TheadComponent={( theadProps: PropsWithChildren, ) => ( )} ThComponent={(thProps: PropsWithChildren) => ( )} /> ), [ collectionView, props.filters, sorted, onSortedChange, columns, getReactTableComponentProps, isLoading, pivotBy, getTbodyComponent, getTrComponent, getTrGroupComponent, NoDataComponent, setCollectionView, PaginationComponent, cardRender, pageSize, showPagination, theadOnTableViewOnly, ], ) if ('searchInput' in props) { return renderCollection({ passedSearchInput: props.searchInput, passedData: data, }) } else if ('searchFilter' in props) { return ( {({ compareSearch, value, onChange }) => { const filteredData = data.filter(item => props.searchFilter({ item, compareSearch, value }), ) return renderCollection({ passedSearchInput: ( ), passedData: filteredData, }) }} ) } else { throw new Error( 'Need to pass searchInput or searchFilter prop to Collection.', ) } } const theadOverrides = css` ${TheadComponentContainer} { ${flexFlow('row', 'wrap')}; height: auto; min-height: 40px; padding: 4px 26px; } ` const CollectionTheadComponent: FC = ({ style, collectionView, tableViewOnly, ...otherProps }) => { switch (collectionView) { case CollectionView.Card: return tableViewOnly ? null : case CollectionView.Table: return } } const thComponentOverrides = css` width: auto; padding-right: 27px; &:first-of-type { padding-left: 6px; } ${ResizerComponent} { display: none; } ${StyledButton} { ${baseSecondaryStyles(ThemeColors.BrandSecondary)}; color: ${getColor(Colors.Black74a)}; border-radius: ${BorderRadius.Small}px 0 0 ${BorderRadius.Small}px; margin-top: 4px; margin-bottom: 4px; &.is-active { ${basePrimaryStyles(ThemeColors.BrandSecondary)} } &:last-of-type { border-radius: 0 ${BorderRadius.Small}px ${BorderRadius.Small}px 0; overflow: visible; transform: translateX(1px); } &:first-of-type { border-radius: ${BorderRadius.Small}px 0 0 ${BorderRadius.Small}px; overflow: hidden; transform: translateX(0); &::before { content: none; } } } ` const CollectionThComponent: FC = ({ style, collectionView, ...otherProps }) => { switch (collectionView) { case CollectionView.Card: return case CollectionView.Table: return } } const FilterContainer = styled.section` ${flexFlow('row')} flex-grow: 1; justify-content: flex-end; `