add a new reusable component - Table
This commit is contained in:
parent
91b7cd89d9
commit
9b128e1e57
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
|
@ -2,3 +2,8 @@ export enum Role {
|
|||
Admin = 'admin',
|
||||
User = 'user',
|
||||
}
|
||||
|
||||
export enum DateFormat {
|
||||
DayMonthAndYear = 'yyyy-MM-dd',
|
||||
HourMinutesDayMonthAndYear = 'yyyy-MM-dd HH:mm',
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
Reference in New Issue