Skip to content

Commit

Permalink
feat: added PaginatedDataGrid
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinGhadyani-Okta committed Jun 21, 2023
1 parent 39ee64d commit 673cf0e
Show file tree
Hide file tree
Showing 10 changed files with 1,068 additions and 175 deletions.
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/odyssey-react-mui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@mui/utils": "^5.11.2",
"@okta/odyssey-design-tokens": "workspace:*",
"i18next": "^22.4.15",
"material-react-table": "^1.12.1",
"material-react-table": "^1.14.0",
"react-i18next": "^12.2.2"
},
"devDependencies": {
Expand Down
48 changes: 28 additions & 20 deletions packages/odyssey-react-mui/src/InfinitelyScrolledDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { Typography } from "@mui/material";
import { AlertProps, TableContainerProps, Typography } from "@mui/material";
import MaterialReactTable, {
type MRT_ColumnFiltersState,
type MRT_RowSelectionState,
Expand All @@ -29,11 +29,10 @@ import {
} from "react";
import { Trans, useTranslation } from "react-i18next";

export type DefaultMaterialReactTableData = Record<string, unknown>;

export type MaterialReactTableProps<
TData extends DefaultMaterialReactTableData
> = Parameters<typeof MaterialReactTable<TData>>[0];
import type {
DefaultMaterialReactTableData,
MaterialReactTableProps,
} from "./materialReactTableTypes";

export type InfinitelyScrolledDataGridProps<
TData extends DefaultMaterialReactTableData
Expand Down Expand Up @@ -176,6 +175,27 @@ const InfinitelyScrolledDataGrid = <
[rowSelection, state]
);

const muiTableContainerProps: TableContainerProps = useMemo(
() => ({
onScroll: (event: UIEvent<HTMLDivElement>) =>
fetchMoreOnBottomReached(event.target as HTMLDivElement),
ref: tableContainerRef,
sx: { maxHeight: String(500 / 14).concat("rem") },
}),
[fetchMoreOnBottomReached]
);

const muiToolbarAlertBannerProps: AlertProps = useMemo(
() =>
hasError
? {
children: t("datagrid.error"),
severity: "error",
}
: {},
[hasError, t]
);

return (
<MaterialReactTable
columns={columns}
Expand All @@ -187,20 +207,8 @@ const InfinitelyScrolledDataGrid = <
enableSorting={false}
getRowId={getRowId}
initialState={initialState}
muiTableContainerProps={{
onScroll: (event: UIEvent<HTMLDivElement>) =>
fetchMoreOnBottomReached(event.target as HTMLDivElement),
ref: tableContainerRef,
sx: { maxHeight: String(500 / 14).concat("rem") },
}}
muiToolbarAlertBannerProps={
hasError
? {
children: t("datagrid.error"),
color: "error",
}
: undefined
}
muiTableContainerProps={muiTableContainerProps}
muiToolbarAlertBannerProps={muiToolbarAlertBannerProps}
onColumnFiltersChange={setColumnFilters}
onGlobalFilterChange={setGlobalFilter}
onRowSelectionChange={setRowSelection}
Expand Down
300 changes: 300 additions & 0 deletions packages/odyssey-react-mui/src/PaginatedDataGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
/*!
* Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import {
AlertProps,
TableContainerProps,
TablePaginationProps,
Typography,
} from "@mui/material";
import MaterialReactTable, {
type MRT_ColumnFiltersState,
type MRT_RowSelectionState,
type MRT_TableInstance,
type MRT_Virtualizer,
} from "material-react-table";
import {
FunctionComponent,
memo,
UIEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Trans, useTranslation } from "react-i18next";

import type {
DefaultMaterialReactTableData,
MaterialReactTableProps,
} from "./materialReactTableTypes";

export type PaginatedDataGridProps<
TData extends DefaultMaterialReactTableData
> = {
columns: MaterialReactTableProps<TData>["columns"];
data: MaterialReactTableProps<TData>["data"];
fetchMoreData?: () => void;
getRowId?: MaterialReactTableProps<TData>["getRowId"];
hasError?: boolean;
hasRowSelection?: boolean;
initialState?: MaterialReactTableProps<TData>["initialState"];
isFetching?: boolean;
onGlobalFilterChange?: MaterialReactTableProps<TData>["onGlobalFilterChange"];
onPaginationChange?: MaterialReactTableProps<TData>["onPaginationChange"];
onRowSelectionChange?: MaterialReactTableProps<TData>["onRowSelectionChange"];
// rowsPerPageOptions?: MaterialReactTableProps<TData>["muiTablePaginationProps"]['rowsPerPageOptions'];
state?: MaterialReactTableProps<TData>["state"];
ToolbarButtons?: FunctionComponent<
{ table: MRT_TableInstance<TData> } & unknown
>;
};

// Once the user has scrolled within this many pixels of the bottom of the table, fetch more data if we can.
const scrollAmountBeforeFetchingData = 400;

const initialPagination = {
pageIndex: 0,
pageSize: 10,
};

const PaginatedDataGrid = <TData extends DefaultMaterialReactTableData>({
columns,
data,
fetchMoreData,
getRowId,
hasError,
hasRowSelection,
initialState,
isFetching,
onGlobalFilterChange,
onPaginationChange,
onRowSelectionChange: onRowSelectionChangeProp,
state,
ToolbarButtons,
}: PaginatedDataGridProps<TData>) => {
const { t } = useTranslation();

const tableContainerRef = useRef<HTMLDivElement>(null);

const rowVirtualizerInstanceRef =
useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null);

const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
[]
);

const [globalFilter, setGlobalFilter] = useState<string>();

useEffect(() => {
if (globalFilter) {
onGlobalFilterChange?.(globalFilter);
}
}, [globalFilter, onGlobalFilterChange]);

const totalFetchedRows = data.length ?? 0;

// const fetchMoreOnBottomReached = useCallback(
// (containerRefElement?: HTMLDivElement | null) => {
// if (containerRefElement) {
// const { scrollHeight, scrollTop, clientHeight } = containerRefElement;

// if (
// scrollHeight - scrollTop - clientHeight <
// scrollAmountBeforeFetchingData &&
// !isFetching
// ) {
// fetchMoreData?.();
// }
// }
// },
// [fetchMoreData, isFetching]
// );

useEffect(() => {
try {
// Scroll to top of table when sorting or filters change.
rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
} catch (error) {
console.error(error);
}
}, [columnFilters, globalFilter]);

// Check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data.
// useEffect(() => {
// fetchMoreOnBottomReached(tableContainerRef.current);
// }, [fetchMoreOnBottomReached]);

const renderBottomToolbarCustomActions = useCallback(
() =>
fetchMoreData ? (
<Typography>
<Trans
count={totalFetchedRows}
i18nKey="datagrid.fetchedrows.text"
values={{
totalRows: totalFetchedRows,
}}
/>
</Typography>
) : (
<Typography>
<Trans
count={totalFetchedRows}
i18nKey="datagrid.rows.text"
values={{
totalRows: totalFetchedRows,
}}
/>
</Typography>
),
[fetchMoreData, totalFetchedRows]
);

const renderTopToolbarCustomActions = useCallback<
Exclude<
MaterialReactTableProps<TData>["renderTopToolbarCustomActions"],
undefined
>
>(
({ table }) => <>{ToolbarButtons && <ToolbarButtons table={table} />}</>,
[ToolbarButtons]
);

const [rowSelection, setRowSelection] = useState<MRT_RowSelectionState>({});

useEffect(() => {
onRowSelectionChangeProp?.(rowSelection);
}, [onRowSelectionChangeProp, rowSelection]);

const [pagination, setPagination] = useState(
initialState?.pagination || initialPagination
);

const stuffPage = useCallback((p) => {
setPagination((op) => {
const newP = p(op);
console.log("p", newP);
return newP;
});
}, []);

useEffect(() => {
console.log("pagination updated");
const numberOfPages = Math.floor(data.length / pagination.pageSize);

if (!isFetching && pagination.pageIndex > numberOfPages - 1) {
console.log("fetching more data");
fetchMoreData?.();
}
}, [
data.length,
fetchMoreData,
isFetching,
pagination.pageIndex,
pagination.pageSize,
]);

useEffect(() => {
onPaginationChange?.({
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
});
}, [onPaginationChange, pagination.pageIndex, pagination.pageSize]);

// const modifiedInitialState = useMemo(
// () => ({
// pagination,
// ...initialState,
// }),
// [initialState, pagination]
// );

const modifiedState = useMemo(
() => ({
...state,
pagination: {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
},
rowSelection,
}),
[pagination.pageIndex, pagination.pageSize, rowSelection, state]
);

// const muiTableContainerProps: TableContainerProps = useMemo(
// () => ({
// onScroll: (event: UIEvent<HTMLDivElement>) =>
// fetchMoreOnBottomReached(event.target as HTMLDivElement),
// ref: tableContainerRef,
// }),
// [fetchMoreOnBottomReached]
// );

const muiToolbarAlertBannerProps: AlertProps = useMemo(
() =>
hasError
? {
children: t("datagrid.error"),
severity: "error",
}
: {},
[hasError, t]
);

const muiTablePaginationProps: Partial<
Omit<TablePaginationProps, "rowsPerPage">
> = useMemo(
() => ({
rowsPerPageOptions: [],
showFirstButton: false,
showLastButton: false,
}),
[]
);

return (
<MaterialReactTable
columns={columns}
data={data}
enableMultiRowSelection={hasRowSelection}
enablePagination
enableRowSelection={hasRowSelection}
enableSorting={false}
getRowId={getRowId}
initialState={initialState}
// manualPagination
// muiTableContainerProps={muiTableContainerProps}
muiTablePaginationProps={muiTablePaginationProps}
muiToolbarAlertBannerProps={muiToolbarAlertBannerProps}
onColumnFiltersChange={setColumnFilters}
onGlobalFilterChange={setGlobalFilter}
onPaginationChange={stuffPage}
onRowSelectionChange={setRowSelection}
renderBottomToolbarCustomActions={renderBottomToolbarCustomActions}
renderTopToolbarCustomActions={renderTopToolbarCustomActions}
rowVirtualizerInstanceRef={rowVirtualizerInstanceRef}
rowVirtualizerProps={{ overscan: 4 }}
state={modifiedState}
/>
);
};

const MemoizedPaginatedDataGrid = memo(
PaginatedDataGrid
) as typeof PaginatedDataGrid;

// @ts-expect-error | This is going to error because the component isn't and can't be defined as a `FunctionComponent`, and therefore, doesn't have a `displayName` prop.
MemoizedPaginatedDataGrid.displayName = "PaginatedDataGrid";

export { MemoizedPaginatedDataGrid as PaginatedDataGrid };
Loading

0 comments on commit 673cf0e

Please sign in to comment.