add a new reusable component - Table

This commit is contained in:
Dawid Wysokiński 2021-03-08 21:37:47 +01:00
parent 91b7cd89d9
commit 9b128e1e57
14 changed files with 561 additions and 7 deletions

View File

@ -10,7 +10,7 @@ const CurrentUser = () => {
<Box pr={0.5} pl={0.5}>
<Typography variant="h6" align="center">
Jesteś zalogowany jako: <br /> <strong>{user?.displayName}</strong> (ID:{' '}
{user?.id}).
{user?.id})
</Typography>
</Box>
);

View File

@ -35,11 +35,13 @@ const TopBar = ({ className, openSidebar }: Props) => {
<MenuIcon />
</IconButton>
</Hidden>
<Typography variant="h4">
<Link color="inherit" to={ROUTE.DASHBOARD_PAGE}>
Zdam Egzamin Zawodowy
</Link>
</Typography>
<Hidden xsDown>
<Typography variant="h4">
<Link color="inherit" to={ROUTE.DASHBOARD_PAGE}>
Zdam Egzamin Zawodowy
</Link>
</Typography>
</Hidden>
</div>
<div className={classes.rightSideContainer}>
<Button color="inherit" onClick={signOut}>

158
src/common/Table/Table.tsx Normal file
View File

@ -0,0 +1,158 @@
import React from 'react';
import { validateRowsPerPage, isObjKey } from './helpers';
import { Action, Column, OrderDirection } from './types';
import {
Table as MUITable,
TableBody,
TableProps,
TableBodyProps,
TableContainer,
} from '@material-ui/core';
import TableHead from './TableHead';
import TableRow from './TableRow';
import TableLoading from './TableLoading';
import TableEmpty from './TableEmpty';
import TableFooter, { TableFooterProps } from './TableFooter';
export interface Props<T> {
columns: Column<T>[];
actions?: Action<T>[];
data: T[];
orderBy?: string;
orderDirection?: OrderDirection;
selection?: boolean;
idFieldName?: string;
getRowKey?: (row: T, index: number) => string | number | null | undefined;
onRequestSort?: (
orderBy: string,
orderDirection: OrderDirection
) => void | Promise<void>;
onSelect?: (rows: T[]) => void;
loading?: boolean;
tableProps?: TableProps;
tableBodyProps?: TableBodyProps;
footerProps?: TableFooterProps;
hideFooter?: boolean;
size?: 'medium' | 'small';
selected?: T[];
}
function Table<T>({
columns,
data,
orderBy = '',
orderDirection = 'asc',
onRequestSort,
idFieldName = 'id',
selection = false,
loading = false,
actions = [],
tableBodyProps = {},
tableProps = {},
hideFooter = false,
footerProps = {},
size,
selected,
onSelect,
getRowKey,
}: Props<T>) {
const headColumns =
actions.length > 0
? [...columns, { field: 'action', label: 'Akcje' }]
: columns;
const preparedFooterProps = {
page: 0,
rowsPerPage: validateRowsPerPage(
footerProps?.rowsPerPage,
footerProps?.rowsPerPageOptions
),
count: data.length,
size: size,
...footerProps,
};
const isSelected = (row: T): boolean => {
return (
Array.isArray(selected) &&
selected.some(
otherRow =>
isObjKey(otherRow, idFieldName) &&
isObjKey(row, idFieldName) &&
otherRow[idFieldName] === row[idFieldName]
)
);
};
return (
<TableContainer>
<MUITable size={size} {...tableProps}>
<TableHead
columns={headColumns}
selection={selection}
orderBy={orderBy}
orderDirection={orderDirection}
onRequestSort={onRequestSort}
size={size}
onSelectAll={() => {
if (onSelect) {
onSelect(data);
}
}}
allSelected={selected?.length === data.length}
/>
<TableBody {...tableBodyProps}>
{loading ? (
<TableLoading
columns={headColumns}
size={size}
rowsPerPage={preparedFooterProps.rowsPerPage}
/>
) : data.length > 0 ? (
data.map((item, index) => {
return (
<TableRow
key={
getRowKey
? getRowKey(item, index)
: isObjKey(item, idFieldName)
? item[idFieldName] + ''
: index
}
index={index}
row={item}
actions={actions}
selected={isSelected(item)}
selection={selection}
columns={columns}
size={size}
onSelect={row => {
if (onSelect) {
onSelect([row]);
}
}}
/>
);
})
) : (
<TableEmpty />
)}
</TableBody>
{!hideFooter && (
<TableFooter
size={size}
{...preparedFooterProps}
count={
loading
? preparedFooterProps.page *
(preparedFooterProps.rowsPerPage + 1)
: preparedFooterProps.count
}
/>
)}
</MUITable>
</TableContainer>
);
}
export default Table;

View File

@ -0,0 +1,15 @@
import React from 'react';
import { TableRow, TableCell, Typography } from '@material-ui/core';
function TableEmpty() {
return (
<TableRow>
<TableCell colSpan={100}>
<Typography align="center">Brak danych do wyświetlenia</Typography>
</TableCell>
</TableRow>
);
}
export default TableEmpty;

View File

@ -0,0 +1,71 @@
import React from 'react';
import {
TablePagination,
TableRow,
TableFooter as MUITableFooter,
} from '@material-ui/core';
export interface TableFooterProps {
page?: number;
count?: number;
onChangePage?: (page: number) => void;
rowsPerPage?: number;
onChangeRowsPerPage?: (limit: number) => void;
rowsPerPageOptions?: Array<number | { value: number; label: string }>;
size?: 'small' | 'medium';
}
export const ROWS_PER_PAGE_DEFAULT = 25;
export const ROWS_PER_PAGE_OPTIONS_DEFAULT = [25, 50, 100];
function TableFooter({
onChangePage,
page = 0,
count = 0,
onChangeRowsPerPage,
rowsPerPageOptions = ROWS_PER_PAGE_OPTIONS_DEFAULT,
rowsPerPage = ROWS_PER_PAGE_DEFAULT,
size = 'small',
}: TableFooterProps) {
const handlePageChange = (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number
) => {
if (onChangePage) {
onChangePage(page);
}
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
if (onChangeRowsPerPage) {
onChangeRowsPerPage(parseInt(e.target.value));
}
};
return (
<MUITableFooter>
<TableRow>
<TablePagination
count={count}
page={page}
onChangePage={handlePageChange}
onChangeRowsPerPage={handleRowsPerPageChange}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={rowsPerPageOptions}
size={size}
colSpan={100}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} z ${count}`
}
labelRowsPerPage="Wierszy na stronę"
nextIconButtonText="Następna strona"
backIconButtonText="Poprzednia strona"
/>
</TableRow>
</MUITableFooter>
);
}
export default TableFooter;

View File

@ -0,0 +1,93 @@
import React from 'react';
import { Column, OrderDirection } from './types';
import {
TableHead as MUITableHead,
TableRow,
TableCell,
TableSortLabel,
Checkbox,
SortDirection,
} from '@material-ui/core';
export interface Props {
columns: Column[];
selection: boolean;
onSelectAll?: () => void;
allSelected: boolean;
orderDirection: OrderDirection;
orderBy: string;
size?: 'small' | 'medium';
onRequestSort?: (
property: string,
orderDirection: OrderDirection
) => void | Promise<void>;
}
function TableHead({
columns,
onSelectAll,
orderBy = '',
orderDirection = 'asc',
selection = false,
allSelected = false,
onRequestSort,
size = 'medium',
}: Props) {
const createSortHandler = (property: string) => () => {
if (onRequestSort) {
if (property === orderBy) {
onRequestSort(property, orderDirection === 'asc' ? 'desc' : 'asc');
} else {
onRequestSort(property, 'asc');
}
}
};
const handleSelectAll = () => {
if (onSelectAll) {
onSelectAll();
}
};
return (
<MUITableHead>
<TableRow>
{selection && (
<TableCell size={size} padding="checkbox">
<Checkbox checked={allSelected} onClick={handleSelectAll} />
</TableCell>
)}
{columns.map(col => {
return (
<TableCell
size={size}
key={col.field}
padding={col.disablePadding ? 'none' : 'default'}
align={col.align ? col.align : 'left'}
sortDirection={
(orderBy === col.field
? orderDirection
: false) as SortDirection
}
>
{col.sortable ? (
<TableSortLabel
active={orderBy === col.field}
onClick={createSortHandler(col.field)}
direction={orderBy === col.field ? orderDirection : 'asc'}
>
{col.label ?? col.field}
</TableSortLabel>
) : (
col.label ?? col.field
)}
</TableCell>
);
})}
</TableRow>
</MUITableHead>
);
}
export default TableHead;

View File

@ -0,0 +1,31 @@
import React, { Fragment } from 'react';
import { Column } from './types';
import { TableRow, TableCell } from '@material-ui/core';
import { Skeleton } from '@material-ui/lab';
export interface Props {
rowsPerPage: number;
columns: Column[];
size?: 'small' | 'medium';
}
function TableLoading({ rowsPerPage, columns, size = 'medium' }: Props) {
return (
<Fragment>
{new Array(rowsPerPage).fill(0).map((_, index) => {
return (
<TableRow key={index}>
{columns.map(col => (
<TableCell size={size} key={col.field}>
<Skeleton variant="text" />
</TableCell>
))}
</TableRow>
);
})}
</Fragment>
);
}
export default TableLoading;

View File

@ -0,0 +1,101 @@
import React from 'react';
import { get, isString, isNumber } from 'lodash';
import { format } from 'date-fns';
import formatNumber from 'utils/formatNumber';
import { DateFormat } from 'config/app';
import { TableRow, TableCell, Checkbox, Tooltip } from '@material-ui/core';
import { Action, Column } from './types';
export interface Props<T> {
actions: Action<T>[];
columns: Column<T>[];
row: T;
selection: boolean;
selected: boolean;
size?: 'small' | 'medium';
index: number;
onSelect?: (row: T) => void;
}
function EnhancedTableRow<T>({
actions,
columns,
row,
selection = false,
selected = false,
onSelect,
size = 'medium',
index,
}: Props<T>) {
const handleSelect = () => {
if (onSelect) {
onSelect(row);
}
};
const formatValue = (v: string | number | Date, type: Column['type']) => {
if ((isString(v) || isNumber(v) || v instanceof Date) && type === 'date') {
return format(new Date(v), DateFormat.DayMonthAndYear);
}
if (
(isString(v) || isNumber(v) || v instanceof Date) &&
type === 'datetime'
) {
return format(new Date(v), DateFormat.HourMinutesDayMonthAndYear);
}
if ((isString(v) || isNumber(v)) && type === 'number') {
return formatNumber('commas', v);
}
return v;
};
return (
<TableRow>
{selection && (
<TableCell size={size} padding="checkbox">
<Checkbox checked={selected} onClick={handleSelect} />
</TableCell>
)}
{columns.map(col => {
const val = get(row, col.field, '');
return (
<TableCell
size={size}
key={col.field}
padding={col.disablePadding ? 'none' : 'default'}
align={col.align ? col.align : 'left'}
>
{col.valueFormatter
? col.valueFormatter(row, index)
: col.type
? formatValue(val, col.type)
: val}
</TableCell>
);
})}
{actions.length > 0 && (
<TableCell size={size}>
{actions.map((action, index) => {
const icon =
typeof action.icon === 'function'
? action.icon(row, index)
: action.icon;
return action.tooltip ? (
<div key={index}>
<Tooltip key={index} title={action.tooltip}>
<span>{icon}</span>
</Tooltip>
</div>
) : (
<div key={index}>{icon}</div>
);
})}
</TableCell>
)}
</TableRow>
);
}
export default EnhancedTableRow;

View File

@ -0,0 +1,27 @@
import React from 'react';
import clsx from 'clsx';
import { makeStyles } from '@material-ui/core/styles';
import { Toolbar, ToolbarProps } from '@material-ui/core';
export type Props = ToolbarProps;
const useStyles = makeStyles(theme => {
return {
toolbar: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
};
});
function TableToolbar({ children, className, ...rest }: Props) {
const classes = useStyles();
return (
<Toolbar {...rest} className={clsx(classes.toolbar, className)}>
{children}
</Toolbar>
);
}
export default TableToolbar;

View File

@ -0,0 +1,22 @@
import {
ROWS_PER_PAGE_DEFAULT,
ROWS_PER_PAGE_OPTIONS_DEFAULT,
} from './TableFooter';
export const validateRowsPerPage = (
rowsPerPage: number = ROWS_PER_PAGE_DEFAULT,
rowsPerPageOptions: Array<
number | { value: number; label: string }
> = ROWS_PER_PAGE_OPTIONS_DEFAULT
) => {
const opt =
rowsPerPageOptions.find(opt =>
typeof opt === 'number' ? rowsPerPage === opt : opt.value === rowsPerPage
) ??
(typeof rowsPerPageOptions[0] === 'number'
? rowsPerPageOptions[0]
: rowsPerPageOptions[0].value);
return typeof opt === 'number' ? opt : opt.value;
};
export const isObjKey = <T>(obj: T, key: any): key is keyof T => key in obj;

16
src/common/Table/types.ts Normal file
View File

@ -0,0 +1,16 @@
export type Action<T> = {
icon: React.ReactNode | ((row: T, i: number) => React.ReactNode);
tooltip?: string;
};
export type Column<T = any> = {
field: string;
label?: string;
sortable?: boolean;
valueFormatter?: (v: T, i: number) => React.ReactNode;
disablePadding?: boolean;
type?: 'normal' | 'number' | 'datetime' | 'date';
align?: 'left' | 'right' | 'center';
};
export type OrderDirection = 'asc' | 'desc';

View File

@ -2,3 +2,8 @@ export enum Role {
Admin = 'admin',
User = 'user',
}
export enum DateFormat {
DayMonthAndYear = 'yyyy-MM-dd',
HourMinutesDayMonthAndYear = 'yyyy-MM-dd HH:mm',
}

View File

@ -1,7 +1,7 @@
import AppLayout from 'common/AppLayout/AppLayout';
const DashboardPage = () => {
return <AppLayout>DashboardPage</AppLayout>;
return <AppLayout>I'll add something here in the future.</AppLayout>;
};
export default DashboardPage;

13
src/utils/formatNumber.ts Normal file
View File

@ -0,0 +1,13 @@
const formatNumber = (variant: 'commas', v: number | string): string => {
if (typeof v === 'string') {
v = parseFloat(v);
}
switch (variant) {
case 'commas':
return v.toLocaleString('en');
default:
return v + '';
}
};
export default formatNumber;