Merge pull request #5 from AntlerEngineering/react-data-grid

React data grid
This commit is contained in:
shamsmosowi
2019-09-23 07:19:36 +10:00
committed by GitHub
15 changed files with 1506 additions and 581 deletions

View File

@@ -13,6 +13,7 @@
"@types/node": "12.7.4",
"@types/ramda": "^0.26.21",
"@types/react": "^16.9.2",
"@types/react-data-grid": "^4.0.3",
"@types/react-dom": "16.9.0",
"@types/react-router-dom": "^4.3.5",
"@types/react-sortable-hoc": "^0.6.5",
@@ -25,6 +26,8 @@
"lodash": "^4.17.15",
"ramda": "^0.26.1",
"react": "^16.9.0",
"react-data-grid": "^6.1.0",
"react-data-grid-addons": "^6.1.0",
"react-dom": "^16.9.0",
"react-dropzone": "^10.1.8",
"react-router-dom": "^5.0.1",

View File

@@ -4,17 +4,13 @@ import { Checkbox } from "@material-ui/core";
// TODO: Create an interface for props
const CheckBox = (props: any) => {
const { columnData, cellData, cellActions, rowData, rowIndex } = props;
const { value, row, onSubmit } = props;
return (
<Checkbox
checked={cellData}
name={`checkBox-controlled-${row.id}`}
checked={value}
onChange={e => {
cellActions.updateFirestore({
rowIndex,
value: !cellData,
docRef: rowData.ref,
fieldName: columnData.fieldName,
});
onSubmit(row.ref, !value);
}}
/>
);

View File

@@ -1,45 +1,40 @@
import React from "react";
import DateFnsUtils from "@date-io/date-fns";
import { Button } from "@material-ui/core";
import { FieldType } from ".";
import {
MuiPickersUtilsProvider,
// KeyboardTimePicker,
// KeyboardDatePicker,
DatePicker,
DateTimePicker,
} from "@material-ui/pickers";
// TODO: Create an interface for props
const Date = (props: any) => {
const {
isFocusedCell,
columnData,
cellData,
cellActions,
rowData,
rowIndex,
} = props;
const { value, row, onSubmit, fieldType } = props;
function handleDateChange(date: Date | null) {
if (date) {
const cell = {
rowIndex,
value: date,
docRef: rowData.ref,
fieldName: columnData.fieldName,
};
cellActions.updateFirestore(cell);
onSubmit(row.ref, date);
}
}
if (!cellData && !isFocusedCell) return <Button>click to set</Button>;
else
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
{fieldType === FieldType.date ? (
<DatePicker
value={cellData && cellData.toDate()}
value={value ? value.toDate() : null}
onChange={handleDateChange}
emptyLabel="select a date"
/>
</MuiPickersUtilsProvider>
);
) : (
<DateTimePicker
value={value ? value.toDate() : null}
onChange={handleDateChange}
emptyLabel="select a time"
/>
)}
</MuiPickersUtilsProvider>
);
};
export default Date;

View File

@@ -1,45 +0,0 @@
import React from "react";
import DateFnsUtils from "@date-io/date-fns";
import { Button } from "@material-ui/core";
import {
MuiPickersUtilsProvider,
// KeyboardTimePicker,
// KeyboardDatePicker,
DateTimePicker,
} from "@material-ui/pickers";
// TODO: Create an interface for props
const DateTime = (props: any) => {
const {
isFocusedCell,
columnData,
cellData,
cellActions,
rowData,
rowIndex,
} = props;
function handleDateChange(date: Date | null) {
if (date) {
const cell = {
rowIndex,
value: date,
docRef: rowData.ref,
fieldName: columnData.fieldName,
};
cellActions.updateFirestore(cell);
}
}
if (!cellData && !isFocusedCell) return <Button>click to set</Button>;
else
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DateTimePicker
value={cellData && cellData.toDate()}
onChange={handleDateChange}
emptyLabel="select a date"
/>
</MuiPickersUtilsProvider>
);
};
export default DateTime;

View File

@@ -0,0 +1,16 @@
import React from "react";
import ExpandIcon from "@material-ui/icons/AspectRatio";
import IconButton from "@material-ui/core/IconButton";
const UrlLink = (props: any) => {
const { value, cellActions } = props;
return value ? (
<>
<IconButton>
<ExpandIcon />
</IconButton>
<p>{value}</p>
</>
) : null;
};
export default UrlLink;

View File

@@ -4,18 +4,15 @@ import { TextField } from "@material-ui/core";
// TODO: Create an interface for props
const Number = (props: any) => {
const { isFocusedCell, cellData, cellActions } = props;
if (isFocusedCell)
return (
<TextField
autoFocus
type="number"
defaultValue={cellData}
onChange={e => {
cellActions.update(e.target.value);
}}
/>
);
else return <p>{cellData}</p>;
const { value, cellActions } = props;
return (
<TextField
autoFocus
type="number"
defaultValue={value}
onChange={e => {}}
/>
);
// else return <p>{cellData}</p>;
};
export default Number;

View File

@@ -3,19 +3,14 @@ import MuiRating from "@material-ui/lab/Rating";
// TODO: Create an interface for props
const Rating = (props: any) => {
const { columnData, cellData, cellActions, rowData, rowIndex } = props;
const { value, row, onSubmit } = props;
return (
<MuiRating
name={`rating-controlled-${columnData.fieldName}-${rowIndex}`}
value={cellData}
// TODO: make it unique for each
name={`rating-controlled-${row.id}`}
value={value}
onChange={(event, newValue) => {
const cell = {
rowIndex,
value: newValue,
docRef: rowData.ref,
fieldName: columnData.fieldName,
};
cellActions.updateFirestore(cell);
onSubmit(row.ref, newValue);
}}
/>
);

View File

@@ -1,21 +0,0 @@
import React from "react";
import { TextField } from "@material-ui/core";
// TODO: Create an interface for props
const SimpleText = (props: any) => {
const { isFocusedCell, cellData, cellActions } = props;
if (isFocusedCell)
return (
<TextField
autoFocus
defaultValue={cellData}
onChange={e => {
cellActions.update(e.target.value);
}}
/>
);
else return <p>{cellData}</p>;
};
export default SimpleText;

View File

@@ -0,0 +1,17 @@
import React from "react";
import EditIcon from "@material-ui/icons/Edit";
// TODO: regex validating url
// ^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$
const UrlLink = (props: any) => {
const { value, cellActions } = props;
return value ? (
<>
<EditIcon />
<a href={value} target="_blank">
{value}
</a>
</>
) : null;
};
export default UrlLink;

View File

@@ -1,262 +0,0 @@
import React, { useEffect } from "react";
import clsx from "clsx";
import { Theme, WithStyles } from "@material-ui/core/styles";
import {
TableCell as MuiTableCell,
createStyles,
withStyles,
Paper,
Button,
} from "@material-ui/core";
import {
AutoSizer,
Column,
Table as MuiTable,
TableCellRenderer,
TableHeaderProps,
} from "react-virtualized";
import { FieldType, getFieldIcon } from "./Fields";
import ColumnDrawer from "./ColumnDrawer";
import TableCell from "../components/TableCell";
import useCell, { Cell } from "../hooks/useFiretable/useCell";
import useFiretable from "../hooks/useFiretable";
const styles = (theme: Theme) =>
createStyles({
flexContainer: {
display: "flex",
alignItems: "center",
boxSizing: "border-box",
},
tableRow: {
cursor: "pointer",
},
tableRowHover: {
"&:hover": {
backgroundColor: theme.palette.grey[200],
},
},
tableCell: {
flex: 1,
},
noClick: {
cursor: "initial",
},
});
interface ColumnData {
columnData: any;
dataKey: string;
label: string;
numeric?: boolean;
width: number;
}
interface Row {
index: number;
}
interface MuiVirtualizedTableProps extends WithStyles<typeof styles> {
columns: ColumnData[];
focusedCell: Cell | null;
cellActions: any;
headerHeight?: number;
onRowClick?: () => void;
rowCount: number;
rowGetter: (row: Row) => any;
rowHeight?: number;
}
class MuiVirtualizedTable extends React.PureComponent<
MuiVirtualizedTableProps
> {
static defaultProps = {
headerHeight: 48,
rowHeight: 40,
};
getRowClassName = ({ index }: Row) => {
const { classes, onRowClick } = this.props;
return clsx(classes.tableRow, classes.flexContainer, {
[classes.tableRowHover]: index !== -1 && onRowClick != null,
});
};
cellRenderer: TableCellRenderer = ({
cellData,
columnData,
columnIndex,
dataKey,
isScrolling,
rowData,
rowIndex,
}) => {
const {
columns,
classes,
rowHeight,
onRowClick,
cellActions,
focusedCell,
} = this.props;
const fieldType = columnData.fieldType;
return (
<TableCell
fieldType={fieldType}
rowIndex={rowIndex}
rowData={rowData}
columnData={columnData}
classes={classes}
cellActions={cellActions}
cellData={cellData}
onRowClick={onRowClick}
rowHeight={rowHeight}
columnIndex={columnIndex}
columns={columns}
focusedCell={focusedCell}
/>
);
};
headerRenderer = ({
label,
columnData,
dataKey,
columnIndex,
}: TableHeaderProps & { columnIndex: number }) => {
const { headerHeight, columns, classes } = this.props;
return (
<MuiTableCell
component="div"
className={clsx(
classes.tableCell,
classes.flexContainer,
classes.noClick
)}
variant="head"
style={{ height: headerHeight }}
align={columns[columnIndex].numeric || false ? "right" : "left"}
>
{dataKey === "add" ? (
<ColumnDrawer
columns={columns}
addColumn={columnData.actions.addColumn}
/>
) : (
<Button size="small">
{getFieldIcon(columnData.fieldType)} {label}
</Button>
)}
</MuiTableCell>
);
};
render() {
const {
classes,
columns,
rowHeight,
headerHeight,
...tableProps
} = this.props;
return (
<AutoSizer>
{({ height, width }) => (
<>
<MuiTable
height={height}
width={width}
rowHeight={rowHeight!}
headerHeight={headerHeight!}
{...tableProps}
rowClassName={this.getRowClassName}
>
{[
...columns.map(({ dataKey, ...other }, index) => {
return (
<Column
key={dataKey}
headerRenderer={headerProps =>
this.headerRenderer({
...headerProps,
columnIndex: index,
})
}
className={classes.flexContainer}
cellRenderer={this.cellRenderer}
dataKey={dataKey}
{...other}
/>
);
}),
]}
</MuiTable>
</>
)}
</AutoSizer>
);
}
}
const VirtualizedTable = withStyles(styles)(MuiVirtualizedTable);
// TODO: Create an interface for props
export default function Table(props: any) {
const { collection } = props;
const { tableState, tableActions } = useFiretable(collection);
useEffect(() => {
tableActions.table.set(collection);
}, [collection]);
if (tableState.columns)
return (
<>
<Paper style={{ height: 400, width: "100%" }}>
<VirtualizedTable
focusedCell={tableState.cell}
cellActions={tableActions.cell}
rowCount={tableState.rows.length}
rowGetter={({ index }) => tableState.rows[index]}
columns={[
...tableState.columns.map(
(column: {
fieldName: string;
columnName: string;
type: FieldType;
}) => ({
width: 200,
label: column.columnName,
dataKey: column.fieldName,
columnData: {
fieldType: column.type,
fieldName: column.fieldName,
actions: {},
},
})
),
{
width: 80,
label: "add",
dataKey: "add",
columnData: {
fieldType: "DELETE",
actions: {
addColumn: tableActions.column.add,
deleteRow: tableActions.row.delete,
},
},
},
]}
/>
</Paper>
<Button onClick={tableActions.row.add}>Add Row</Button>
</>
);
else return <>insert loading Skeleton here</>;
}

View File

@@ -0,0 +1,109 @@
import React from "react";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Popper from "@material-ui/core/Popper";
import Fade from "@material-ui/core/Fade";
import Paper from "@material-ui/core/Paper";
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
import { TextField, Grid } from "@material-ui/core";
import { FIELDS } from "../Fields";
const useStyles = makeStyles(Theme =>
createStyles({
container: {
padding: 10
},
typography: {
padding: 1
},
header: {
position: "absolute",
left: 0,
top: 0
//zIndex: 100000
},
button: {
// margin: theme.spacing(1)
},
root: {
display: "flex",
flexWrap: "wrap"
},
formControl: {
margin: Theme.spacing(1),
minWidth: 120
},
selectEmpty: {
marginTop: Theme.spacing(2)
}
})
);
const HeaderPopper = (props: any) => {
const { anchorEl, column, handleClose } = props;
console.log(column);
const [values, setValues] = React.useState({
age: "",
name: "hai"
});
console.log(props);
const classes = useStyles();
function handleChange(
event: React.ChangeEvent<{ name?: string; value: unknown }>
) {
setValues(oldValues => ({
...oldValues,
[event.target.name as string]: event.target.value
}));
}
if (column) {
return (
<Popper
id={`id-${column.name}`}
open={!!anchorEl}
anchorEl={anchorEl}
transition
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper className={classes.container}>
<Grid container direction="column">
<TextField label="Column name" defaultValue={column.name} />
<FormControl className={classes.formControl}>
<InputLabel htmlFor="age-simple">Field Type</InputLabel>
<Select
value={FIELDS[0].type}
onChange={handleChange}
inputProps={{
name: "age",
id: "age-simple"
}}
>
{FIELDS.map((field: any) => {
return (
<MenuItem value={field.type}>
{field.icon} {field.name}
</MenuItem>
);
})}
</Select>
<Button>Add</Button>
<Button color="secondary" onClick={handleClose}>
cancel
</Button>
</FormControl>
</Grid>
</Paper>
</Fade>
)}
</Popper>
);
}
return <div />;
};
export default HeaderPopper;

View File

@@ -0,0 +1,85 @@
import React from "react";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import Popper from "@material-ui/core/Popper";
import Fade from "@material-ui/core/Fade";
import Paper from "@material-ui/core/Paper";
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
import { TextField } from "@material-ui/core";
const useStyles = makeStyles(Theme =>
createStyles({
typography: {
padding: 1
},
header: {
position: "absolute",
left: 0,
top: 0
//zIndex: 100000
},
button: {
// margin: theme.spacing(1)
},
root: {
display: "flex",
flexWrap: "wrap"
},
formControl: {
margin: Theme.spacing(1),
minWidth: 120
},
selectEmpty: {
marginTop: Theme.spacing(2)
}
})
);
const NewColumnPopper = (props: any) => {
const { anchorEl, column } = props;
const [values, setValues] = React.useState({
age: "",
name: "hai"
});
console.log(props);
const classes = useStyles();
function handleChange(
event: React.ChangeEvent<{ name?: string; value: unknown }>
) {
setValues(oldValues => ({
...oldValues,
[event.target.name as string]: event.target.value
}));
}
return (
<Popper id={"id"} open={!!anchorEl} anchorEl={anchorEl} transition>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper>
<TextField label="Column name" />
<FormControl className={classes.formControl}>
<InputLabel htmlFor="age-simple">Age</InputLabel>
<Select
value={values.age}
onChange={handleChange}
inputProps={{
name: "age",
id: "age-simple"
}}
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
</Paper>
</Fade>
)}
</Popper>
);
};
export default NewColumnPopper;

View File

@@ -0,0 +1,181 @@
import React, { useState } from "react";
import ReactDataGrid from "react-data-grid";
import useFiretable from "../../hooks/useFiretable";
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
import IconButton from "@material-ui/core/IconButton";
import Button from "@material-ui/core/Button";
import EditIcon from "@material-ui/icons/Edit";
import HeaderPopper from "./HeaderPopper";
import { FieldType } from "../Fields";
import Date from "../Fields/Date";
import Rating from "../Fields/Rating";
import CheckBox from "../Fields/CheckBox";
import UrlLink from "../Fields/UrlLink";
const useStyles = makeStyles(Theme =>
createStyles({
typography: {
padding: 1
},
header: {
position: "absolute",
left: 0,
top: 0
},
headerButton: {
width: "100%"
}
})
);
const copyPaste = (props: any) => {
console.log(props);
};
const editable = (fieldType: FieldType) => {
switch (fieldType) {
case FieldType.date:
case FieldType.dateTime:
case FieldType.rating:
case FieldType.checkBox:
return false;
default:
return true;
}
};
const onSubmit = (fieldName: string) => (
ref: firebase.firestore.DocumentReference,
value: any
) => {
if (value !== null || value !== undefined) {
ref.update({ [fieldName]: value });
}
};
const DateFormatter = (fieldName: string, fieldType: FieldType) => (
props: any
) => {
return (
<Date {...props} onSubmit={onSubmit(fieldName)} fieldType={fieldType} />
);
};
const formatter = (fieldType: FieldType, fieldName: string) => {
switch (fieldType) {
case FieldType.date:
case FieldType.dateTime:
return DateFormatter(fieldName, fieldType);
case FieldType.rating:
return (props: any) => {
return <Rating {...props} onSubmit={onSubmit(fieldName)} />;
};
case FieldType.checkBox:
return (props: any) => {
return <CheckBox {...props} onSubmit={onSubmit(fieldName)} />;
};
case FieldType.url:
return (props: any) => {
return <UrlLink {...props} onSubmit={onSubmit(fieldName)} />;
};
default:
return false;
}
};
function Table(props: any) {
const { collection } = props;
const { tableState, tableActions } = useFiretable(collection);
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const [header, setHeader] = useState<any | null>();
const handleCloseHeader = () => {
setHeader(null);
setAnchorEl(null);
};
const handleClick = (headerProps: any) => (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
handleCloseHeader();
setAnchorEl(event.currentTarget);
setHeader(headerProps);
};
const onGridRowsUpdated = (props: any) => {
const { fromRowData, updated } = props;
fromRowData.ref.update(updated);
};
const onCellSelected = (args: any) => {
handleCloseHeader();
console.log(args);
};
const headerRenderer = (props: any) => {
switch (props.column.key) {
case "new":
return (
<Button onClick={handleClick(props)} className={classes.header}>
{props.column.name}
</Button>
);
default:
return (
<div className={classes.header}>
<Button
className={classes.headerButton}
onClick={handleClick(props)}
aria-label="edit"
>
{props.column.name} <EditIcon />
</Button>
</div>
);
}
};
if (tableState.columns) {
let columns = tableState.columns.map((column: any) => ({
key: column.fieldName,
name: column.columnName,
editable: editable(column.type),
resizeable: true,
// frozen: column.fieldName === "cohort",
headerRenderer: headerRenderer,
formatter: formatter(column.type, column.fieldName),
width: 200,
...column
}));
columns.push({
key: "new",
name: "Add column",
width: 160,
headerRenderer: headerRenderer
});
const rows = tableState.rows;
return (
<>
<ReactDataGrid
columns={columns}
rowGetter={i => rows[i]}
rowsCount={rows.length}
onGridRowsUpdated={onGridRowsUpdated}
enableCellSelect={true}
onCellCopyPaste={copyPaste}
minHeight={500}
onCellSelected={onCellSelected}
/>
<Button onClick={tableActions.row.add}>Add Row</Button>
<HeaderPopper
handleClose={handleCloseHeader}
anchorEl={anchorEl}
column={header && header.column}
/>
</>
);
} else return <p>Loading</p>;
}
export default Table;

View File

@@ -1,157 +0,0 @@
import React, { useState } from "react";
import clsx from "clsx";
import {
TableCell as MuiTableCell,
Switch,
IconButton,
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import { FieldType } from "./Fields";
import SimpleText from "./Fields/SimpleText";
import CheckBox from "./Fields/CheckBox";
import Number from "./Fields/Number";
import Rating from "./Fields/Rating";
import Date from "./Fields/Date";
import DateTime from "./Fields/DateTime";
import Image from "./Fields/Image";
// TODO: Create an interface for props
const TableCell = (props: any) => {
const {
fieldType,
rowIndex,
rowData,
columnData,
classes,
cellActions,
cellData,
onRowClick,
rowHeight,
columnIndex,
columns,
focusedCell,
} = props;
const isFocusedCell =
focusedCell &&
focusedCell.fieldName === columnData.fieldName &&
focusedCell.rowIndex === rowIndex;
const renderCell = () => {
switch (fieldType) {
case FieldType.checkBox:
return (
<CheckBox
rowIndex={rowIndex}
rowData={rowData}
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
case FieldType.rating:
return (
<Rating
rowIndex={rowIndex}
rowData={rowData}
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
case FieldType.image:
return (
<Image
rowIndex={rowIndex}
rowData={rowData}
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
case FieldType.date:
return (
<Date
rowIndex={rowIndex}
rowData={rowData}
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
case FieldType.dateTime:
return (
<DateTime
rowIndex={rowIndex}
rowData={rowData}
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
case FieldType.number:
return (
<Number
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
default:
return (
<SimpleText
isFocusedCell={isFocusedCell}
cellData={cellData}
cellActions={cellActions}
columnData={columnData}
/>
);
}
};
if (fieldType === "DELETE")
return (
<IconButton
aria-label="delete"
onClick={() => {
columnData.actions.deleteRow(rowIndex, rowData.id);
}}
>
<DeleteIcon />
</IconButton>
);
return (
<MuiTableCell
component="div"
className={clsx(classes.tableCell, classes.flexContainer, {
[classes.noClick]: onRowClick == null,
})}
variant="body"
onClick={() => {
// set focusedCell on click
cellActions.set({
rowIndex,
docRef: rowData.ref,
fieldName: columnData.fieldName,
value: cellData,
});
}}
style={{ height: rowHeight }}
align={
(columnIndex != null && columns[columnIndex].numeric) || false
? "right"
: "left"
}
>
{renderCell()}
</MuiTableCell>
);
};
export default TableCell;

1100
yarn.lock

File diff suppressed because it is too large Load Diff