diff --git a/src/client/actions/index.js b/src/client/actions/index.js index bedcb091672da182e8f7400645298ce5d43070fd..f8606e25665fd4e0f519900787f406abb02ff669 100644 --- a/src/client/actions/index.js +++ b/src/client/actions/index.js @@ -159,8 +159,9 @@ export const fetchPlaceFailed = (error) => ({ }); // Facet -export const openFacetDialog = () => ({ +export const openFacetDialog = property => ({ type: OPEN_FACET_DIALOG, + property }); export const closeFacetDialog = () => ({ type: CLOSE_FACET_DIALOG, diff --git a/src/client/components/EnhancedTable.js b/src/client/components/EnhancedTable.js new file mode 100644 index 0000000000000000000000000000000000000000..b5e6830eeaa50d89521c6ed561f6e9066159d3bb --- /dev/null +++ b/src/client/components/EnhancedTable.js @@ -0,0 +1,354 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Paper from '@material-ui/core/Paper'; +import Checkbox from '@material-ui/core/Checkbox'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import DeleteIcon from '@material-ui/icons/Delete'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; + +let counter = 0; +function createData(name, calories, fat, carbs, protein) { + counter += 1; + return { id: counter, name, calories, fat, carbs, protein }; +} + +function desc(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function stableSort(array, cmp) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = cmp(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map(el => el[0]); +} + +function getSorting(order, orderBy) { + return order === 'desc' ? (a, b) => desc(a, b, orderBy) : (a, b) => -desc(a, b, orderBy); +} + +const rows = [ + { id: 'name', numeric: false, disablePadding: true, label: 'Dessert (100g serving)' }, + { id: 'calories', numeric: true, disablePadding: false, label: 'Calories' }, + { id: 'fat', numeric: true, disablePadding: false, label: 'Fat (g)' }, + { id: 'carbs', numeric: true, disablePadding: false, label: 'Carbs (g)' }, + { id: 'protein', numeric: true, disablePadding: false, label: 'Protein (g)' }, +]; + +class EnhancedTableHead extends React.Component { + createSortHandler = property => event => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount } = this.props; + + return ( + <TableHead> + <TableRow> + <TableCell padding="checkbox"> + <Checkbox + indeterminate={numSelected > 0 && numSelected < rowCount} + checked={numSelected === rowCount} + onChange={onSelectAllClick} + /> + </TableCell> + {rows.map(row => { + return ( + <TableCell + key={row.id} + numeric={row.numeric} + padding={row.disablePadding ? 'none' : 'default'} + sortDirection={orderBy === row.id ? order : false} + > + <Tooltip + title="Sort" + placement={row.numeric ? 'bottom-end' : 'bottom-start'} + enterDelay={300} + > + <TableSortLabel + active={orderBy === row.id} + direction={order} + onClick={this.createSortHandler(row.id)} + > + {row.label} + </TableSortLabel> + </Tooltip> + </TableCell> + ); + }, this)} + </TableRow> + </TableHead> + ); + } +} + +EnhancedTableHead.propTypes = { + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.string.isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +const toolbarStyles = theme => ({ + root: { + paddingRight: theme.spacing.unit, + }, + highlight: + theme.palette.type === 'light' + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actions: { + color: theme.palette.text.secondary, + }, + title: { + flex: '0 0 auto', + }, +}); + +let EnhancedTableToolbar = props => { + const { numSelected, classes } = props; + + return ( + <Toolbar + className={classNames(classes.root, { + [classes.highlight]: numSelected > 0, + })} + > + <div className={classes.title}> + {numSelected > 0 ? ( + <Typography color="inherit" variant="subtitle1"> + {numSelected} selected + </Typography> + ) : ( + <Typography variant="h6" id="tableTitle"> + Nutrition + </Typography> + )} + </div> + <div className={classes.spacer} /> + <div className={classes.actions}> + {numSelected > 0 ? ( + <Tooltip title="Delete"> + <IconButton aria-label="Delete"> + <DeleteIcon /> + </IconButton> + </Tooltip> + ) : ( + <Tooltip title="Filter list"> + <IconButton aria-label="Filter list"> + <FilterListIcon /> + </IconButton> + </Tooltip> + )} + </div> + </Toolbar> + ); +}; + +EnhancedTableToolbar.propTypes = { + classes: PropTypes.object.isRequired, + numSelected: PropTypes.number.isRequired, +}; + +EnhancedTableToolbar = withStyles(toolbarStyles)(EnhancedTableToolbar); + +const styles = theme => ({ + root: { + width: '100%', + marginTop: theme.spacing.unit * 3, + }, + table: { + minWidth: 1020, + }, + tableWrapper: { + overflowX: 'auto', + }, +}); + +class EnhancedTable extends React.Component { + state = { + order: 'asc', + orderBy: 'calories', + selected: [], + data: [ + createData('Cupcake', 305, 3.7, 67, 4.3), + createData('Donut', 452, 25.0, 51, 4.9), + createData('Eclair', 262, 16.0, 24, 6.0), + createData('Frozen yoghurt', 159, 6.0, 24, 4.0), + createData('Gingerbread', 356, 16.0, 49, 3.9), + createData('Honeycomb', 408, 3.2, 87, 6.5), + createData('Ice cream sandwich', 237, 9.0, 37, 4.3), + createData('Jelly Bean', 375, 0.0, 94, 0.0), + createData('KitKat', 518, 26.0, 65, 7.0), + createData('Lollipop', 392, 0.2, 98, 0.0), + createData('Marshmallow', 318, 0, 81, 2.0), + createData('Nougat', 360, 19.0, 9, 37.0), + createData('Oreo', 437, 18.0, 63, 4.0), + ], + page: 0, + rowsPerPage: 5, + }; + + handleRequestSort = (event, property) => { + const orderBy = property; + let order = 'desc'; + + if (this.state.orderBy === property && this.state.order === 'desc') { + order = 'asc'; + } + + this.setState({ order, orderBy }); + }; + + handleSelectAllClick = event => { + if (event.target.checked) { + this.setState(state => ({ selected: state.data.map(n => n.id) })); + return; + } + this.setState({ selected: [] }); + }; + + handleClick = (event, id) => { + const { selected } = this.state; + const selectedIndex = selected.indexOf(id); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1), + ); + } + + this.setState({ selected: newSelected }); + }; + + handleChangePage = (event, page) => { + this.setState({ page }); + }; + + handleChangeRowsPerPage = event => { + this.setState({ rowsPerPage: event.target.value }); + }; + + isSelected = id => this.state.selected.indexOf(id) !== -1; + + render() { + const { classes } = this.props; + const { data, order, orderBy, selected, rowsPerPage, page } = this.state; + const emptyRows = rowsPerPage - Math.min(rowsPerPage, data.length - page * rowsPerPage); + + return ( + <Paper className={classes.root}> + <EnhancedTableToolbar numSelected={selected.length} /> + <div className={classes.tableWrapper}> + <Table className={classes.table} aria-labelledby="tableTitle"> + <EnhancedTableHead + numSelected={selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={this.handleSelectAllClick} + onRequestSort={this.handleRequestSort} + rowCount={data.length} + /> + <TableBody> + {stableSort(data, getSorting(order, orderBy)) + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map(n => { + const isSelected = this.isSelected(n.id); + return ( + <TableRow + hover + onClick={event => this.handleClick(event, n.id)} + role="checkbox" + aria-checked={isSelected} + tabIndex={-1} + key={n.id} + selected={isSelected} + > + <TableCell padding="checkbox"> + <Checkbox checked={isSelected} /> + </TableCell> + <TableCell component="th" scope="row" padding="none"> + {n.name} + </TableCell> + <TableCell numeric>{n.calories}</TableCell> + <TableCell numeric>{n.fat}</TableCell> + <TableCell numeric>{n.carbs}</TableCell> + <TableCell numeric>{n.protein}</TableCell> + </TableRow> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 49 * emptyRows }}> + <TableCell colSpan={6} /> + </TableRow> + )} + </TableBody> + </Table> + </div> + <TablePagination + rowsPerPageOptions={[5, 10, 25]} + component="div" + count={data.length} + rowsPerPage={rowsPerPage} + page={page} + backIconButtonProps={{ + 'aria-label': 'Previous Page', + }} + nextIconButtonProps={{ + 'aria-label': 'Next Page', + }} + onChangePage={this.handleChangePage} + onChangeRowsPerPage={this.handleChangeRowsPerPage} + /> + </Paper> + ); + } +} + +EnhancedTable.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(EnhancedTable); diff --git a/src/client/components/FacetDialog.js b/src/client/components/FacetDialog.js index 18446dabfc0fc41b2efe09cdd8b8330d4234aacf..ec3aa0022d2702ebec7374efb623f157e93d1211 100644 --- a/src/client/components/FacetDialog.js +++ b/src/client/components/FacetDialog.js @@ -27,7 +27,9 @@ class FacetDialog extends React.Component { handleClose = () => this.props.closeFacetDialog(); render() { - const { classes } = this.props; + const { classes, facet } = this.props; + const label = facet.facetOptions[facet.activeFacet] == null ? '' : facet.facetOptions[facet.activeFacet].label; + const facetValues = facet.facetValues[facet.activeFacet] == null ? [] : facet.facetValues[facet.activeFacet]; return ( <Dialog classes={{ paper: classes.dialogPaper }} @@ -38,14 +40,14 @@ class FacetDialog extends React.Component { disableRestoreFocus={true} > <DialogTitle disableTypography={true}> - <Typography variant="h6">{this.props.propertyLabel}</Typography> + <Typography variant="h6">{label}</Typography> </DialogTitle> <DialogContent> - {this.props.facet.fetchingFacet ? + {this.props.facet.fetchingFacet || facetValues.length == 0 ? <CircularProgress style={{ color: purple[500] }} thickness={5} /> : <Tree - data={this.props.facet.facetValues.creationPlace} + data={facetValues} fetchFacet={this.props.fetchFacet} updateFilter={this.props.updateFilter} />} @@ -57,8 +59,6 @@ class FacetDialog extends React.Component { FacetDialog.propTypes = { classes: PropTypes.object.isRequired, - property: PropTypes.string.isRequired, - propertyLabel: PropTypes.string.isRequired, fetchFacet: PropTypes.func.isRequired, facet: PropTypes.object.isRequired, updateFilter: PropTypes.func.isRequired, diff --git a/src/client/components/ResultTableHead.js b/src/client/components/ResultTableHead.js index 3f3a6001a588049b521a6bb99b76a31121ba3856..28635480b9c59996f3a4d68dceeb61183ba58ba5 100644 --- a/src/client/components/ResultTableHead.js +++ b/src/client/components/ResultTableHead.js @@ -97,8 +97,8 @@ class ResultTableHead extends React.Component { this.setState({ order, orderBy }); }; - handleFacetButtonOnClick = () => { - this.props.openFacetDialog(); + handleFacetButtonOnClick = property => () => { + this.props.openFacetDialog(property); } // <TableSortLabel @@ -138,7 +138,7 @@ class ResultTableHead extends React.Component { {column.filter && <Tooltip title={'Filter ' + column.label.toLowerCase()}> <IconButton - onClick={this.handleFacetButtonOnClick} + onClick={this.handleFacetButtonOnClick(column.property)} aria-label="Filter" > <FilterListIcon /> diff --git a/src/client/containers/MapApp.js b/src/client/containers/MapApp.js index 2715d4a6e0ae9c42e16436e180c5608b53c1c132..19121b91750bf452d097ed42ec2fadf7a1f19045 100644 --- a/src/client/containers/MapApp.js +++ b/src/client/containers/MapApp.js @@ -120,8 +120,6 @@ let MapApp = (props) => { </React.Fragment> </Router> <FacetDialog - property='creationPlace' - propertyLabel='Creation place' facet={props.facet} fetchFacet={props.fetchFacet} updateFilter={props.updateFilter} diff --git a/src/client/reducers/facet.js b/src/client/reducers/facet.js index ceabd9758bd41617d7e39b83bf91f0325a9e1499..427c3425123a4ba9f848e051bfc872e97f73eb05 100644 --- a/src/client/reducers/facet.js +++ b/src/client/reducers/facet.js @@ -9,12 +9,16 @@ import { export const INITIAL_STATE = { facetOptions : { creationPlace: { + id: 'creationPlace', + label: 'Creation place', predicate: '^frbroo:R18_created/crm:P7_took_place_at', hierarchical: true, }, author: { - hierarchical: false, + id: 'author', + label: 'Author', predicate: '^frbroo:R18_created/crm:P14_carried_out_by', + hierarchical: false } }, facetValues : { @@ -26,13 +30,18 @@ export const INITIAL_STATE = { author: new Set(), }, fetchingFacet : false, - facetDialogOpen: false + facetDialogOpen: false, + activeFacet: '' }; const facet = (state = INITIAL_STATE, action) => { switch (action.type) { case OPEN_FACET_DIALOG: - return { ...state, facetDialogOpen: true }; + return { + ...state, + facetDialogOpen: true, + activeFacet: action.property + }; case CLOSE_FACET_DIALOG: return { ...state, facetDialogOpen: false }; case FETCH_FACET: