wisemapping-frontend/packages/webapp/src/components/maps-page/maps-list/index.tsx

491 lines
17 KiB
TypeScript
Raw Normal View History

2021-02-01 03:04:50 +01:00
import React, { useEffect } from 'react'
2021-01-28 02:27:19 +01:00
import { useStyles } from './styled';
2021-01-25 19:39:53 +01:00
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
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 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 StarRateRoundedIcon from '@material-ui/icons/StarRateRounded';
import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
import { CSSProperties } from 'react';
import { useSelector } from 'react-redux';
2021-02-05 08:05:46 +01:00
import { activeInstance } from '../../../redux/clientSlice';
2021-01-28 02:27:19 +01:00
import { useMutation, useQuery, useQueryClient } from 'react-query';
2021-02-02 07:15:32 +01:00
import { ErrorInfo, MapInfo } from '../../../client';
import Client from '../../../client';
2021-01-25 19:39:53 +01:00
import ActionChooser, { ActionType } from '../action-chooser';
2021-01-30 09:51:02 +01:00
import ActionDispatcher from '../action-dispatcher';
2021-02-02 07:15:32 +01:00
import { Button, InputBase, Link } from '@material-ui/core';
2021-01-30 09:51:02 +01:00
import SearchIcon from '@material-ui/icons/Search';
2021-01-31 09:29:36 +01:00
import moment from 'moment'
2021-02-02 09:20:35 +01:00
import { Filter, LabelFilter } from '..';
2021-02-05 22:15:36 +01:00
import { FormattedMessage, useIntl } from 'react-intl';
2021-02-02 07:15:32 +01:00
import { DeleteOutlined, LabelTwoTone } from '@material-ui/icons';
2021-02-05 08:05:46 +01:00
import Alert from '@material-ui/lab/Alert';
2021-01-25 19:39:53 +01:00
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
type Order = 'asc' | 'desc';
function getComparator<Key extends keyof any>(
order: Order,
orderBy: Key,
2021-02-05 02:40:41 +01:00
): (a: { [key in Key]: number | string | boolean | number[] | undefined }, b: { [key in Key]: number | string | number[] | boolean }) => number {
2021-01-25 19:39:53 +01:00
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
interface HeadCell {
id: keyof MapInfo;
2021-01-28 02:27:19 +01:00
label?: string;
2021-01-25 19:39:53 +01:00
numeric: boolean;
2021-01-31 09:29:36 +01:00
style?: CSSProperties;
2021-01-25 19:39:53 +01:00
}
interface EnhancedTableProps {
classes: ReturnType<typeof useStyles>;
numSelected: number;
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof MapInfo) => void;
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
order: Order;
orderBy: string;
rowCount: number;
}
function EnhancedTableHead(props: EnhancedTableProps) {
2021-02-05 22:15:36 +01:00
const intl = useIntl();
2021-01-25 19:39:53 +01:00
const { classes, onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } = props;
const createSortHandler = (property: keyof MapInfo) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
2021-02-05 22:15:36 +01:00
const headCells: HeadCell[] = [
{ id: 'title', numeric: false, label: intl.formatMessage({ id: 'map.name', defaultMessage: 'Name' }) },
{ id: 'labels', numeric: false },
{ id: 'creator', numeric: false, label: intl.formatMessage({ id: 'map.creator', defaultMessage: 'Creator' }), style: { width: '70px', whiteSpace: 'nowrap' } },
{ id: 'modified', numeric: true, label: intl.formatMessage({ id: 'map.last-update', defaultMessage: 'Last Update' }), style: { width: '70px', whiteSpace: 'nowrap' } }
];
2021-01-25 19:39:53 +01:00
return (
<TableHead>
<TableRow>
2021-01-28 02:27:19 +01:00
<TableCell padding='checkbox' key='select' style={{ width: '20px' }} className={classes.headerCell}>
2021-01-25 19:39:53 +01:00
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
size='small'
inputProps={{ 'aria-label': 'select all desserts' }}
/>
</TableCell>
2021-01-31 08:42:16 +01:00
<TableCell padding='checkbox' key='starred' className={classes.headerCell}></TableCell>
2021-01-28 02:27:19 +01:00
{headCells.map((headCell) => {
2021-01-31 08:42:16 +01:00
return (<TableCell
2021-01-25 19:39:53 +01:00
key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false}
style={headCell.style}
2021-01-28 02:27:19 +01:00
className={classes.headerCell}
2021-01-25 19:39:53 +01:00
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}>
{headCell.label}
2021-02-06 09:45:33 +01:00
{orderBy === headCell.id && (
2021-01-25 19:39:53 +01:00
<span className={classes.visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</span>
2021-02-06 09:45:33 +01:00
)}
2021-01-25 19:39:53 +01:00
</TableSortLabel>
2021-01-31 08:42:16 +01:00
</TableCell>)
})}
<TableCell padding='checkbox' key='action' className={classes.headerCell}></TableCell>
2021-01-25 19:39:53 +01:00
</TableRow>
</TableHead>
);
}
type ActionPanelState = {
el: HTMLElement | undefined,
mapId: number
}
2021-02-01 03:04:50 +01:00
interface MapsListProps {
filter: Filter
}
const mapsFilter = (filter: Filter, search: string): ((mapInfo: MapInfo) => boolean) => {
return (mapInfo: MapInfo) => {
// Check for filter condition
let result = false;
switch (filter.type) {
case 'all':
result = true;
break;
case 'starred':
result = mapInfo.starred;
break;
2021-02-02 07:15:32 +01:00
case 'owned':
2021-02-05 18:46:16 +01:00
result = mapInfo.role == 'owner';
2021-02-02 07:15:32 +01:00
break;
case 'shared':
2021-02-05 18:46:16 +01:00
result = mapInfo.role != 'owner';
2021-02-02 07:15:32 +01:00
break;
2021-02-02 09:20:35 +01:00
case 'label':
2021-02-05 02:40:41 +01:00
result = !mapInfo.labels || mapInfo.labels.includes((filter as LabelFilter).label.id)
2021-02-02 09:20:35 +01:00
break;
2021-02-05 18:46:16 +01:00
case 'public':
result = mapInfo.isPublic;
break;
2021-02-01 03:04:50 +01:00
default:
result = false;
}
// Does it match search filter criteria...
if (search && result) {
2021-02-04 19:01:35 +01:00
result = mapInfo.title.toLowerCase().indexOf(search.toLowerCase()) != -1;
2021-02-01 03:04:50 +01:00
}
return result;
}
}
export const MapsList = (props: MapsListProps) => {
2021-01-25 19:39:53 +01:00
const classes = useStyles();
const [order, setOrder] = React.useState<Order>('asc');
2021-02-01 03:04:50 +01:00
const [filter, setFilter] = React.useState<Filter>({ type: 'all' });
2021-01-25 19:39:53 +01:00
const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('modified');
const [selected, setSelected] = React.useState<number[]>([]);
2021-02-01 03:04:50 +01:00
const [searchCondition, setSearchCondition] = React.useState<string>('');
2021-01-25 19:39:53 +01:00
const [page, setPage] = React.useState(0);
2021-02-01 03:04:50 +01:00
const [rowsPerPage, setRowsPerPage] = React.useState(10);
2021-02-02 07:15:32 +01:00
const client: Client = useSelector(activeInstance);
2021-02-05 22:15:36 +01:00
const intl = useIntl();
2021-02-02 07:15:32 +01:00
2021-02-01 03:04:50 +01:00
useEffect(() => {
setSelected([]);
setPage(0);
setFilter(props.filter)
2021-02-02 09:20:35 +01:00
}, [props.filter.type, (props.filter as LabelFilter).label]);
2021-02-01 03:04:50 +01:00
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', async () => {
2021-02-02 07:15:32 +01:00
return await client.fetchAllMaps();
2021-01-25 19:39:53 +01:00
});
2021-02-01 03:04:50 +01:00
const mapsInfo: MapInfo[] = data ? data.filter(mapsFilter(filter, searchCondition)) : [];
2021-01-25 19:39:53 +01:00
const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(undefined);
type ActiveDialog = {
actionType: ActionType;
mapId: number
};
const [activeDialog, setActiveDialog] = React.useState<ActiveDialog | undefined>(undefined);
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof MapInfo) => {
const isAsc = orderBy === property && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(property);
};
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>): void => {
if (event.target.checked) {
const newSelecteds = mapsInfo.map((n) => n.id);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleRowClick = (event: React.MouseEvent<unknown>, id: number): void => {
const selectedIndex = selected.indexOf(id);
let newSelected: number[] = [];
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),
);
}
setSelected(newSelected);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleActionClick = (mapId: number): ((event: any) => void) => {
return (event: any): void => {
setActiveRowAction(
{
mapId: mapId,
el: event.currentTarget
}
);
event.stopPropagation();
};
};
2021-01-28 02:27:19 +01:00
const queryClient = useQueryClient();
const starredMultation = useMutation<void, ErrorInfo, number>((id: number) => {
2021-02-04 23:34:13 +01:00
const map = mapsInfo.find(m => m.id == id);
return client.changeStarred(id, !Boolean(map?.starred));
2021-01-28 02:27:19 +01:00
},
{
onSuccess: () => {
queryClient.invalidateQueries('maps');
},
onError: (error) => {
2021-02-05 02:40:41 +01:00
// @todo ...
2021-01-28 02:27:19 +01:00
// setError(error);
}
}
);
const handleStarred = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: number) => {
event.stopPropagation();
starredMultation.mutate(id);
}
2021-01-25 19:39:53 +01:00
const handleActionMenuClose = (action: ActionType): void => {
if (action) {
const mapId = activeRowAction?.mapId;
setActiveDialog({
actionType: action as ActionType,
mapId: mapId as number
});
}
2021-01-28 02:27:19 +01:00
setActiveRowAction(undefined);
2021-01-25 19:39:53 +01:00
};
2021-02-01 03:04:50 +01:00
const handleOnSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchCondition(e.target.value);
}
2021-01-25 19:39:53 +01:00
2021-02-01 03:04:50 +01:00
const isSelected = (id: number) => selected.indexOf(id) !== -1;
2021-01-25 19:39:53 +01:00
return (
<div className={classes.root}>
2021-02-06 17:27:37 +01:00
<ActionChooser anchor={activeRowAction?.el} onClose={handleActionMenuClose} mapId={activeRowAction?.mapId} />
2021-01-28 02:27:19 +01:00
<Paper className={classes.paper} elevation={0}>
2021-01-30 09:51:02 +01:00
<Toolbar className={classes.toolbar} variant="dense">
<div className={classes.toolbarActions}>
2021-02-06 09:45:33 +01:00
{selected.length > 0 &&
2021-02-02 07:15:32 +01:00
<Tooltip title="Delete selected">
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
disableElevation={true}
startIcon={<DeleteOutlined />}>
<FormattedMessage id="action.delete" defaultMessage="Delete" />
</Button>
</Tooltip>
2021-02-06 09:45:33 +01:00
}
2021-01-30 09:51:02 +01:00
2021-02-06 09:45:33 +01:00
{selected.length > 0 &&
2021-02-02 07:15:32 +01:00
<Tooltip title="Add label to selected">
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
style={{ marginLeft: "10px" }}
disableElevation={true}
startIcon={<LabelTwoTone />}>
<FormattedMessage id="action.label" defaultMessage="Add Label" />
</Button>
2021-01-30 09:51:02 +01:00
</Tooltip>
2021-02-06 09:45:33 +01:00
}
2021-01-30 09:51:02 +01:00
</div>
<div className={classes.toolbarListActions}>
<TablePagination
style={{ float: 'right', border: "0", paddingBottom: "5px" }}
count={mapsInfo.length}
2021-02-02 07:15:32 +01:00
rowsPerPageOptions={[]}
2021-01-30 09:51:02 +01:00
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
component="div"
/>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="Search…"
classes={{
root: classes.searchInputRoot,
input: classes.searchInputInput,
}}
inputProps={{ 'aria-label': 'search' }}
2021-02-01 03:04:50 +01:00
onChange={handleOnSearchChange}
2021-01-30 09:51:02 +01:00
/>
</div>
</div>
</Toolbar>
2021-01-28 02:27:19 +01:00
2021-01-25 19:39:53 +01:00
<TableContainer>
<Table
className={classes.table}
2021-01-28 02:27:19 +01:00
size="small"
2021-01-25 19:39:53 +01:00
stickyHeader
>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={mapsInfo.length}
/>
2021-01-28 02:27:19 +01:00
2021-01-25 19:39:53 +01:00
<TableBody>
2021-02-02 19:54:37 +01:00
{isLoading ? (
2021-02-03 23:27:32 +01:00
<TableRow><TableCell colSpan={6}>Loading ...</TableCell></TableRow>) :
2021-02-02 19:54:37 +01:00
(mapsInfo.length == 0 ?
2021-02-05 08:05:46 +01:00
(<TableRow><TableCell colSpan={6} style={{ textAlign: 'center' }}><FormattedMessage id="maps.empty-result" defaultMessage="No matching record found with the current filter criteria." /></TableCell></TableRow>) :
2021-02-02 19:54:37 +01:00
stableSort(mapsInfo, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row: MapInfo) => {
const isItemSelected = isSelected(row.id);
const labelId = row.id;
return (
<TableRow
hover
onClick={(event: any) => handleRowClick(event, row.id)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.id}
selected={isItemSelected}
style={{ border: "0" }}
>
<TableCell
padding="checkbox"
className={classes.bodyCell}>
<Checkbox
checked={isItemSelected}
inputProps={{ 'aria-labelledby': String(labelId) }}
size="small" />
</TableCell>
<TableCell
padding="checkbox"
className={classes.bodyCell}>
<Tooltip title="Starred">
<IconButton aria-label="Starred" size="small" onClick={(e) => handleStarred(e, row.id)}>
<StarRateRoundedIcon color="action" style={{ color: row.starred ? 'yellow' : 'gray' }} />
</IconButton>
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
<Tooltip title="Open for edition" placement="bottom-start">
2021-02-04 23:34:13 +01:00
<Link href={`/c/maps/${row.id}/edit`} color="textPrimary" underline="always" onClick={(e) => e.stopPropagation()}>
2021-02-04 19:01:35 +01:00
{row.title}
2021-02-02 19:54:37 +01:00
</Link>
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
{row.labels}
</TableCell>
<TableCell className={classes.bodyCell}>
{row.creator}
</TableCell>
<TableCell className={classes.bodyCell}>
<Tooltip title={moment(row.modified).format("lll")} placement="bottom-start">
<span>{moment(row.modified).fromNow()}</span>
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
2021-02-05 22:15:36 +01:00
<Tooltip title={intl.formatMessage({ id: 'map.more-actions', defaultMessage: 'More Actions' })}>
2021-02-02 19:54:37 +01:00
<IconButton aria-label="Others" size="small" onClick={handleActionClick(row.id)}>
<MoreHorizIcon color="action" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
}))}
2021-01-25 19:39:53 +01:00
</TableBody>
</Table>
</TableContainer>
2021-01-30 09:51:02 +01:00
2021-01-25 19:39:53 +01:00
</Paper>
<ActionDispatcher action={activeDialog?.actionType} onClose={() => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1} />
2021-01-28 02:27:19 +01:00
</div >
2021-01-25 19:39:53 +01:00
);
2021-02-05 08:05:46 +01:00
}
const ErrorDialog = (props) => {
return (<Alert severity="error">This is an error alert check it out!</Alert>);
2021-01-25 19:39:53 +01:00
}