/* eslint react-hooks/rules-of-hooks:0 */ import _ from 'lodash'; import i18next from 'i18next'; import PropTypes from 'prop-types'; import React, { useEffect, useState, useRef, Ref } from 'react'; import { ThemeProvider } from 'styled-components'; import { getTheme } from '../utils/theme'; import { Box, Row, Text, Item, Icon, Headers, Content, Container, PaginationBar, PaginationInfo, ContainerTable, LoadingRowItem, ContainerHeaders, DropdownContainer, CloseFilter, RowSeparator, } from './styledComponents'; import { getPaginatedData, sortListBy, useOnClickOutside } from './utils'; import { Column, Props, ActiveFilter } from './interfaces'; import { TYPE_FIELD, getPageLimitOptions, ORDER_TYPE } from './constants'; import Tooltip from '../Tooltip'; import SelectorFilter from '../SelectorFilter'; import EmptyState from './components/EmptyState'; import iconSortedListDesc from '../images/icon-sorted-desc.svg'; import iconSortedListAsc from '../images/icon-sorted-asc.svg'; import iconSort from '../images/icon-sort.svg'; import iconSortedListDescDark from '../images/icon-sorted-desc-dark.svg'; import iconSortedListAscDark from '../images/icon-sorted-asc-dark.svg'; import iconSortDark from '../images/icon-sort-dark.svg'; import ArrowGrey from '../images/icon-arrow-grey.svg'; import DropdownGrey from '../images/icon-dropdown-grey.svg'; import ArrowLeftBlue from '../images/icon-arrow-left-blue.svg'; import CheckMarkBlue from '../images/icon-check-blue.svg'; import DropdownDark from '../images/icon-dropdown-dark.svg'; import ArrowLeftDark from '../images/icon-arrow-left-dark.svg'; import CheckMarkDark from '../images/icon-check-dark.svg'; import { useScrollListener } from '../commons/hooks/useScrollListener'; const icons = { blue: { arrow: ArrowGrey, check: CheckMarkBlue, dropdown: DropdownGrey, navigation: ArrowLeftBlue, sort: { default: iconSort, asc: iconSortedListAsc, desc: iconSortedListDesc, }, }, dark: { arrow: ArrowGrey, check: CheckMarkDark, dropdown: DropdownDark, navigation: ArrowLeftDark, sort: { default: iconSortDark, asc: iconSortedListAscDark, desc: iconSortedListDescDark, }, }, }; const NB_EMPTY_ROWS_LOADING_STATE = 9; function renderActiveFilterIndex(activeFilters, column) { const activeFilterIndex = activeFilters.findIndex( (activeFilter) => activeFilter.propertyKey === column.propertyKey ); const filterIndex = activeFilterIndex !== -1 ? activeFilterIndex + 1 : null; return ( filterIndex && ( ({filterIndex}) ) ); } function renderContent( columns: Column[], // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any[], highligthedRowsId: string[], labelEmptyState: string, displaySeparator: boolean, useCustomRender: boolean ) { return ( <> {data.length ? ( data.map((item) => ( {columns.map(({ name, decimals, propertyKey, type, render }) => { const fieldFormat = TYPE_FIELD[type] || TYPE_FIELD.string; return ( {fieldFormat.format( item, item[propertyKey], decimals, (useCustomRender || type === TYPE_FIELD.currency.name) && render )} ); })} {displaySeparator && } )) ) : ( )} ); } function renderHeader( columns: Column[], orderBy: string, orderType: 'asc' | 'desc', setOrderColumn: (columnId: string | number) => void, selectedFilterId: string | number | object | null, handleFilterSelection: (filterId: string | number) => void, updateFilters: (filterId, propertyKey, doFilter, value) => void, activeFilters: ActiveFilter[], GetOrCreateRef: (id: string | number) => Ref, languageCode: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any[], // eslint-disable-next-line @typescript-eslint/no-explicit-any highlightedRowsId: any[], isScrolling: boolean, displayColumnsFilters: boolean, labelEmptyState: string, theme ) { const highlightedRows = data.filter((item) => highlightedRowsId.includes(item.id) ); return (
{columns.map((column) => { const isColumnSelected = column.id === orderBy; const isFilterActive = !!activeFilters.find( (activeFilter) => activeFilter.propertyKey === column.propertyKey ); return ( setOrderColumn(column.id)} grey={!isColumnSelected} darkBlue={isColumnSelected || isFilterActive} interactive > {column.name} {column.id !== orderBy && ( setOrderColumn(column.id)} width="16" height="16" src={icons[theme.pagination?.icon].sort?.default} alt="icon-order" marginLeft="5px" interactive /> )} {column.id === orderBy && ( setOrderColumn(column.id)} width="16" height="16" src={ orderType === ORDER_TYPE.DESCENDING ? icons[theme.pagination?.icon].sort?.desc : icons[theme.pagination?.icon].sort?.asc } alt="icon-order" marginLeft="5px" interactive /> )} {column.tooltipText && } {column.filterType && displayColumnsFilters && ( <> updateFilters( filterId, column.propertyKey, doFilter, value ) } languageCode={languageCode} /> {renderActiveFilterIndex(activeFilters, column)} )} ); })} {highlightedRows && highlightedRows.length > 0 && renderContent( columns, highlightedRows, highlightedRowsId, labelEmptyState, false, false )}
); } function renderPagination( currentPage: number, totalPage: number, pageLimit: number, isDropDownPageLimitOpened: boolean, nbTotalElements: number, isLoading: boolean, previousPage, nextPage, setPageLimit, setIsDropDownPageLimitOpened, theme, getFirstElementOfPage, getLastElementOfPage ) { return (
!isLoading && setIsDropDownPageLimitOpened(!isDropDownPageLimitOpened) } onKeyDown={() => !isLoading && setIsDropDownPageLimitOpened(!isDropDownPageLimitOpened) } style={{ color: theme.pagination?.dropdown?.color, border: theme.pagination?.dropdown?.border, boxShadow: theme.pagination?.dropdown?.boxShadow, background: theme.pagination?.dropdown?.backgroundColor, borderRadius: theme.pagination?.dropdown?.borderRadius, }} interactive > {isDropDownPageLimitOpened && ( {getPageLimitOptions(nbTotalElements).map((option) => ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
option.callback(setPageLimit, nbTotalElements)} onKeyDown={() => option.callback(setPageLimit, nbTotalElements) } > {option.text} {option.value === pageLimit && ( )}
))}
)} {pageLimit === nbTotalElements ? i18next.t('GENERAL_ALL') : `${pageLimit} ${i18next.t( 'COMPONENT_LIST_PAGINATION_PER_PAGE' )}`}
{getFirstElementOfPage()} - {getLastElementOfPage()}{' '} {i18next.t('COMPONENT_LIST_VIEW_PAGINATOR_ON')} {nbTotalElements}
{`${currentPage} / ${totalPage}`} = totalPage ? 'disabled' : undefined} onClick={nextPage} onKeyDown={nextPage} interactive >
); } function renderLoadingState(columns: Column[], theme) { return ( <> {_.times(NB_EMPTY_ROWS_LOADING_STATE, (index) => ( {columns.map(({ name }) => ( ))} {index > 0 && } ))} ); } const TableView = (props: Props): JSX.Element | null => { const { data, columns, minWidth, maxPerPage, positionTop, positionBottom, languageCode, defaultOrderBy, defaultOrderType, highligthedRowsId, isLoading, displayColumnsFilters, labelEmptyState, theme, } = props; const tableContentRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [activeData, setActiveData] = useState([]); const [orderBy, setOrderBy] = useState(defaultOrderBy); const [orderType, setOrdertType] = useState(defaultOrderType); const [totalPage, setTotalPage] = useState(1); const [pageLimit, setPageLimit] = useState(maxPerPage); const [currentPage, setCurrentPage] = useState(1); const [selectedFilterId, setSelectedFilterId] = useState< string | number | object | null >(null); const [activeFilters, setActiveFilters] = useState([]); const [isDropDownPageLimitOpened, setIsDropDownPageLimitOpened] = useState(false); const [isScrollingContent, setIsScrollingContent] = useState(false); useScrollListener(tableContentRef, (event) => { setIsScrollingContent(event.target.scrollTop !== 0); }); useEffect(() => { i18next.changeLanguage(languageCode); }, [languageCode]); useEffect(() => { if (!data) { return; } setActiveData(data); }, [data]); useEffect(() => { if (!activeData) { return; } const totalPerPage = pageLimit; setTotalPage( Math.max( Math.ceil( activeData.filter((item) => !highligthedRowsId.includes(item.id)) .length / totalPerPage ), 1 ) ); setCurrentPage(1); }, [pageLimit, activeData, highligthedRowsId]); const previousPage = () => { if (currentPage <= 1) { return; } setCurrentPage(currentPage - 1); }; const nextPage = () => { if (currentPage >= totalPage) { return; } setCurrentPage(currentPage + 1); }; const setOrderColumn = (columnId) => { if (orderBy === columnId) { setOrdertType( orderType === ORDER_TYPE.ASCENDING ? ORDER_TYPE.DESCENDING : ORDER_TYPE.ASCENDING ); return; } setOrderBy(columnId); setOrdertType(ORDER_TYPE.ASCENDING); }; if (!columns || !columns.length) { return null; } const references = {}; // create ref to attach to selector Filter using useOnClickOutside hook const GetOrCreateRef = (id) => { if (!Object.prototype.hasOwnProperty.call(references, id)) { references[id] = useOnClickOutside(() => { if (selectedFilterId === id) { setSelectedFilterId(null); } }); } return references[id]; }; /** *************** */ /* Handle Filters */ /** *************** */ const handleFilterSelection = (filterId) => setSelectedFilterId(selectedFilterId === filterId ? null : filterId); const updateFilters = (filterId, propertyKey, doFilter, value) => { // deactivate filter when deselected or submitted incomplete or empty if ( !doFilter || value === null || value.minValue === null || value.maxValue === null || (filterId.toString().startsWith('string-selector-filter') && value.length === 0) ) { setActiveFilters( activeFilters.filter( (activeFilter) => activeFilter.filterId !== filterId ) ); return; } // remove overwritten filter const updatedActiveFilters = activeFilters.filter( (activeFilter) => activeFilter.propertyKey !== propertyKey ); // activate new filter setActiveFilters([ ...updatedActiveFilters, { filterId, propertyKey, doFilter, value }, ]); }; useEffect(() => { if (!activeFilters.length) { setActiveData(data); return; } let filteredData = data; activeFilters.forEach(({ propertyKey, doFilter, value }) => { filteredData = doFilter(filteredData, propertyKey, value); }); setActiveData(filteredData); }, [activeFilters, data]); const sortedData = sortListBy( activeData, columns, orderBy, orderType, highligthedRowsId ); const updatedTheme = getTheme(theme, 'tableView'); const getFirstElementOfPage = () => { return (currentPage - 1) * maxPerPage + 1; }; const getLastElementOfPage = () => { const itemsCount = activeData.filter( (item) => !highligthedRowsId.includes(item.id) ).length; if (currentPage * maxPerPage > itemsCount) { return itemsCount; } return currentPage * maxPerPage; }; return ( {activeFilters.length !== 0 && ( setActiveFilters([])}> {i18next.t('GENERAL_CLEAR_FILTER')} )} {renderHeader( columns, orderBy, orderType, setOrderColumn, selectedFilterId, handleFilterSelection, updateFilters, activeFilters, GetOrCreateRef, languageCode, activeData, isLoading ? [] : highligthedRowsId, // no need to display highlighted rows if data are being fetched isScrollingContent, displayColumnsFilters, labelEmptyState, updatedTheme )} {isLoading && renderLoadingState(columns, updatedTheme)} {!isLoading && renderContent( columns, getPaginatedData( sortedData, currentPage, pageLimit, highligthedRowsId ), highligthedRowsId, labelEmptyState, true, true )} {renderPagination( currentPage, totalPage, pageLimit, isDropDownPageLimitOpened, activeData.filter((item) => !highligthedRowsId.includes(item.id)) .length, isLoading, previousPage, nextPage, setPageLimit, setIsDropDownPageLimitOpened, updatedTheme, getFirstElementOfPage, getLastElementOfPage )} ); }; TableView.propTypes = { // eslint-disable-next-line react/forbid-prop-types theme: PropTypes.objectOf(PropTypes.any), isLoading: PropTypes.bool, minWidth: PropTypes.number, maxPerPage: PropTypes.number, positionTop: PropTypes.number, positionBottom: PropTypes.number, defaultOrderBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), defaultOrderType: PropTypes.string, columns: PropTypes.arrayOf( PropTypes.shape({ render: PropTypes.func, decimals: PropTypes.number, tooltipText: PropTypes.string, name: PropTypes.string.isRequired, type: PropTypes.oneOf(Object.keys(TYPE_FIELD)), propertyKey: PropTypes.string.isRequired, }) ).isRequired, // eslint-disable-next-line react/forbid-prop-types data: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-next-line react/forbid-prop-types highligthedRowsId: PropTypes.arrayOf(PropTypes.any), displayColumnsFilters: PropTypes.bool, languageCode: PropTypes.string, labelEmptyState: PropTypes.string, }; TableView.defaultProps = { isLoading: false, minWidth: 600, maxPerPage: 20, positionTop: 0, positionBottom: 0, defaultOrderBy: null, defaultOrderType: ORDER_TYPE.ASCENDING, highligthedRowsId: [], displayColumnsFilters: true, languageCode: 'fr', labelEmptyState: 'Aucun résultat ne correspond à votre recherche', theme: null, }; export default TableView;