From 276eeded477d1240d9f6df7129b371223ee83916 Mon Sep 17 00:00:00 2001 From: Mustapha CHOURIA Date: Mon, 27 Feb 2023 13:01:46 +0100 Subject: [PATCH] Added load on scroll functionnality for remake_0.1.X --- .../DataGrid/InfiniteScrollObserver.tsx | 69 +++++++ .../Containers/DataGrid/UpDataGrid.tsx | 76 ++++++-- .../Containers/DataGrid/UpPagination.tsx | 91 ++++++---- .../Containers/DataGrid/index.stories.tsx | 168 +++++++++++++++++- 4 files changed, 355 insertions(+), 49 deletions(-) create mode 100644 src/Components/Containers/DataGrid/InfiniteScrollObserver.tsx diff --git a/src/Components/Containers/DataGrid/InfiniteScrollObserver.tsx b/src/Components/Containers/DataGrid/InfiniteScrollObserver.tsx new file mode 100644 index 000000000..75357da4f --- /dev/null +++ b/src/Components/Containers/DataGrid/InfiniteScrollObserver.tsx @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { style } from 'typestyle'; +interface IProps { + children?: any; + upDatagridHeight?: string; + borderRadius?: string; + borderColor?: string; + onScrollStop: (page?: number, take?: number, skip?: number) => void; +} + +export const InfiniteScrollObserver = (props: IProps) => { + const { onScrollStop, borderColor, borderRadius, children, upDatagridHeight } = props; + const [pageNum, setPageNum] = useState(1); + const loader = useRef(null); + + const handleObserver = useCallback(entries => { + const target = entries[0]; + if (target.isIntersecting) { + setPageNum(prev => prev + 1); + onScrollStop(pageNum); + } + }, []); + useEffect(() => { + const option = { + root: null, + rootMargin: '20px', + threshold: 0, + }; + const observer = new IntersectionObserver(handleObserver, option); + if (loader.current) observer.observe(loader.current); + }, [handleObserver]); + + return ( +
+ {children} +
+
+ ); +}; + +const scrollableDataGridStyle = (upDatagridHeight = '700px', borderRadius?: string, borderColor?: string): string => + style({ + border: 'solid 1px', + borderColor: borderColor, + width: '100%', + borderRadius: borderRadius, + overflowY: 'auto', + maxHeight: upDatagridHeight, + position: 'sticky', + top: 0, + $nest: { + table: { + borderCollapse: 'collapse', + }, + thead: { + position: 'sticky', + top: 0, + zIndex: 1000, + }, + tbody: { + position: 'sticky', + top: 0, + }, + th: { + position: 'sticky', + top: 0, + }, + }, + }); diff --git a/src/Components/Containers/DataGrid/UpDataGrid.tsx b/src/Components/Containers/DataGrid/UpDataGrid.tsx index fa13796cc..433bec18a 100644 --- a/src/Components/Containers/DataGrid/UpDataGrid.tsx +++ b/src/Components/Containers/DataGrid/UpDataGrid.tsx @@ -1,31 +1,32 @@ import * as React from 'react'; -import $ from 'jquery'; import classnames from 'classnames'; +import $ from 'jquery'; import { media, style } from 'typestyle'; import axios from 'axios'; -import UpPagination, { UpPaginationProps } from './UpPagination'; -import UpDataGridRowHeader from './UpDataGridRowHeader'; import UpDataGridRow, { ActionFactory } from './UpDataGridRow'; +import UpDataGridRowHeader from './UpDataGridRowHeader'; import { ICellFormatter } from './UpDefaultCellFormatter'; +import UpPagination, { UpPaginationProps } from './UpPagination'; import UpLoadingIndicator from '../../Display/LoadingIndicator'; import UpButton from '../../Inputs/Button/UpButton'; -import { IntentType, WithThemeProps } from '../../../Common/theming/types'; import { ActionType } from '../../../Common/actions'; import UpDefaultTheme, { withTheme } from '../../../Common/theming'; +import { IntentType, WithThemeProps } from '../../../Common/theming/types'; import UpDataGridFooter, { UpDataGridFooterProps } from './UpDataGridFooter'; import UpDataGridHeader, { UpDataGridHeaderProps } from './UpDataGridHeader'; -import { UpDataGridProvider } from './UpDataGridContext'; -import { getTestableComponentProps, TestableComponentProps } from '../../../Common/utils/types'; -import { DeviceSmartphones } from '../../../Common/utils/device'; import { IconName } from '../../../Common/theming/icons'; import { isEmpty } from '../../../Common/utils'; import { DetailsData, DetailsType } from './UpDataGridDetails'; +import { DeviceSmartphones } from '../../../Common/utils/device'; +import { getTestableComponentProps, TestableComponentProps } from '../../../Common/utils/types'; +import { InfiniteScrollObserver } from './InfiniteScrollObserver'; +import { UpDataGridProvider } from './UpDataGridContext'; const WrapperDataGridStyle = style( { @@ -296,6 +297,9 @@ export interface UpDataGridProps extends TestableComponentProps { injectRow?: (previous: any, next: any, colum: Column[]) => JSX.Element; // Event Handler onSortChange?: (c: Column, dir: SortDirection) => void; + onScrollStop?: (page: number, take: number, skip: number) => void; + upDatagridHeight?: string; + loadOnScroll?: boolean; onSelectionChange?: ( lastUpdatedRow: Row, dataSelected: any[], @@ -325,6 +329,8 @@ export interface UpDataGridState { rowsSelected?: Array; lastFetchedDataTime?: Date; data?: any; + currentPage?: number; + refershData?: boolean; } export type SortDirection = 'ASC' | 'DESC'; @@ -412,6 +418,8 @@ class UpDataGrid extends React.Component { + const totalPages = Math.ceil(this.state.total / this.state.take); + if (this.state.currentPage < totalPages) { + if (this.props.onScrollStop) this.props.onScrollStop(page, take, skip); + this.setState( + { currentPage: this.state.currentPage + 1, take, skip, isDataFetching: true, refershData: false }, + () => { + if (this.props.dataSource !== undefined) { + this.fetchData(); + } + } + ); + } + }; + onPageChange = (page: number, take: number, skip: number): void => { if (this.props.paginationProps.onPageChange) this.props.paginationProps.onPageChange(page, take, skip); @@ -673,7 +699,7 @@ class UpDataGrid extends React.Component { + this.setState({ columns: columns, currentPage: 1, refershData: true }, () => { if (this.props.dataSource != undefined) { this.fetchData(); } @@ -727,6 +753,7 @@ class UpDataGrid extends React.Component
@@ -861,7 +888,34 @@ class UpDataGrid extends React.Component - <> + {this.props.loadOnScroll ? ( + + { + this.refTable = r; + }} + className={classnames('up-data-grid-main', DataGridStyle(this.props))} + > + + {rows} +
+
+ ) : ( { this.refTable = r; @@ -881,7 +935,7 @@ class UpDataGrid extends React.Component {rows}
- + )} Array; /** Affihage du nombre de résultats */ @@ -320,7 +322,7 @@ class UpPagination extends React.Component + ) : ( +
  • + e.preventDefault()} href="#"> + {value} + +
  • ); + + return paginationNumbers; }); pageNumberNavigation = ( ); diff --git a/src/Components/Containers/DataGrid/index.stories.tsx b/src/Components/Containers/DataGrid/index.stories.tsx index 795f0d1e1..05dbbd6b5 100644 --- a/src/Components/Containers/DataGrid/index.stories.tsx +++ b/src/Components/Containers/DataGrid/index.stories.tsx @@ -4,7 +4,7 @@ import classnames from 'classnames'; import UpDefaultTheme, { UpThemeInterface } from '../../../Common/theming'; import { WithThemeProps } from '../../../Common/theming/types'; -import UpDataGrid, { Action, Row } from './UpDataGrid'; +import UpDataGrid, { Action, Column, Row, SortDirection } from './UpDataGrid'; import { getRootContainer } from '../../../Common/stories'; import { withKnobs } from '@storybook/addon-knobs'; @@ -537,6 +537,172 @@ export const WithAutoClearSelectionOnDataChanged = (): JSX.Element => { ); }; +export const WithLoadOnScrollEnabled = (): JSX.Element => { + const [isFetching, setIsFetching] = React.useState(false); + const [currentPage, setPage] = React.useState(1); + const [state, setState] = React.useState<{ + data: Array; + total: number; + lastFetchedDataTime: Date | undefined; + previousFetchedPage: number; + }>({ + data: [], + total: 0, + lastFetchedDataTime: undefined, + previousFetchedPage: 0, + }); + + const [previousPage, setPreviousPage] = React.useState(0); + const [currentAllRowsSelected, setAllRowsSelected] = React.useState>([]); + const [isAllRowsSelected, setIsAllRowsSelected] = React.useState(false); + + const fetchData = (): Promise => { + setIsFetching(true); + setState({ ...state, data: [] }); + return fetch('https://jsonplaceholder.typicode.com/posts') + .then(response => response.json()) + .then(data => { + setState({ + data: data.slice((currentPage - 1) * 50, (currentPage - 1) * 50 + 50), + total: data.length, + previousFetchedPage: previousPage, + lastFetchedDataTime: new Date(), + }); + + if (isAllRowsSelected === true && currentPage != previousPage) { + const newSelectedData = dataSelectedToRows(data, currentAllRowsSelected, isAllRowsSelected); + setAllRowsSelected(newSelectedData); + } + + setPreviousPage(currentPage); + + return data; + }) + .then(data => { + setIsFetching(false); + return data; + }); + }; + + React.useEffect(() => { + fetchData(); + }, [currentPage, setPage]); + + const dataSelectedToRows = (data: any[], allRowsSelected: Row[], isAllRowsSelected: boolean): Array => { + const newAllRowsSelected: Row[] = _.clone(allRowsSelected); + data.forEach(item => { + const matchedRow = newAllRowsSelected.find(row => row.value.id == item.id); + if (matchedRow == null) { + newAllRowsSelected.push({ + isSelected: isAllRowsSelected, + value: item, + }); + } else { + matchedRow.isSelected = isAllRowsSelected; + } + }); + + return newAllRowsSelected; + }; + + const updateCurrentAllRowsSelected = (updatedRow: Row, currentAllRowsSelected: Row[]): Array => { + const newSelectedData: Row[] = _.clone(currentAllRowsSelected); + const matchedRow = newSelectedData.find(row => row.value.id == updatedRow.value.id); + if (matchedRow == null) { + newSelectedData.push({ ...updatedRow }); + } else { + matchedRow.isSelected = isAllRowsSelected; + } + return newSelectedData; + }; + + const onSelectionChange = ( + lastUpdatedRow: Row, + dataSelected: any[], + allRowsSelected?: Row[], + isAllRowsSelected?: boolean + ): void => { + let newSelectedData: Row[] = []; + + if (lastUpdatedRow != null) { + newSelectedData = updateCurrentAllRowsSelected(lastUpdatedRow, currentAllRowsSelected); + } else if (isAllRowsSelected != null) { + newSelectedData = dataSelectedToRows(state.data, currentAllRowsSelected, isAllRowsSelected); + } + + if (newSelectedData != null) setAllRowsSelected(newSelectedData.filter(row => row.isSelected)); + + setIsAllRowsSelected(isAllRowsSelected === true); + }; + + return ( + <> + { + setAllRowsSelected([]); + fetchData(); + }} + > + Rafraichir + + {isFetching && } + { + setPreviousPage(currentPage); + setPage(page); + }, + }} + onScrollStop={(page: number, take: number, skip: number): void => { + setPreviousPage(currentPage); + setPage(currentPage + 1); + }} + onSortChange={(c: Column, dir: SortDirection): void => { + setPreviousPage(1); + setPage(1); + }} + isSelectionEnabled={true} + isPaginationEnabled={true} + loadOnScroll={true} + columns={[ + { + label: 'Id', + field: 'id', + isSortable: true, + }, + { + label: 'Titre', + field: 'title', + isSortable: true, + }, + { + label: 'Texte', + field: 'body', + isSortable: true, + }, + { + label: 'Auteur', + field: 'userId', + isSortable: true, + }, + ]} + /> + + ); +}; + export const WithExternalSource = (): JSX.Element => (