611 lines
25 KiB
TypeScript
Raw Normal View History

2021-02-22 22:37:29 -08:00
import React, { useEffect, CSSProperties } from 'react'
import { useStyles } from './styled'
import { useSelector } from 'react-redux'
import { activeInstance, fetchAccount } from '../../../redux/clientSlice'
import { useMutation, useQuery, useQueryClient } from 'react-query'
import Client, { ErrorInfo, MapInfo } from '../../../classes/client'
import ActionChooser, { ActionType } from '../action-chooser'
import ActionDispatcher from '../action-dispatcher'
import dayjs from 'dayjs'
import { Filter, LabelFilter } from '..'
import { FormattedMessage, useIntl } from 'react-intl'
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 Button from '@material-ui/core/Button'
import InputBase from '@material-ui/core/InputBase'
import Link from '@material-ui/core/Link'
import LabelTwoTone from '@material-ui/icons/LabelTwoTone'
import DeleteOutlined from '@material-ui/icons/DeleteOutlined'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import StarRateRoundedIcon from '@material-ui/icons/StarRateRounded'
import SearchIcon from '@material-ui/icons/Search'
2021-02-16 09:11:33 -08:00
2021-02-16 13:09:58 -08:00
// Load fromNow pluggin
2021-02-22 22:37:29 -08:00
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
2021-01-25 10:39:53 -08:00
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
2021-02-22 22:37:29 -08:00
if (b[orderBy] < a[orderBy]) {
return -1
}
if (b[orderBy] > a[orderBy]) {
return 1
}
return 0
2021-01-25 10:39:53 -08:00
}
2021-02-22 22:37:29 -08:00
type Order = 'asc' | 'desc'
2021-01-25 10:39:53 -08:00
2021-02-16 21:51:59 -08:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2021-01-25 10:39:53 -08:00
function getComparator<Key extends keyof any>(
2021-02-22 22:37:29 -08:00
order: Order,
orderBy: Key
): (
a: { [key in Key]: number | string | boolean | number[] | undefined },
b: { [key in Key]: number | string | number[] | boolean }
) => number {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy)
2021-01-25 10:39:53 -08:00
}
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
2021-02-22 22:37:29 -08:00
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])
2021-01-25 10:39:53 -08:00
}
interface HeadCell {
2021-02-22 22:37:29 -08:00
id: keyof MapInfo
label?: string
numeric: boolean
style?: CSSProperties
2021-01-25 10:39:53 -08:00
}
interface EnhancedTableProps {
2021-02-22 22:37:29 -08:00
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
2021-01-25 10:39:53 -08:00
}
function EnhancedTableHead(props: EnhancedTableProps) {
2021-02-22 22:37:29 -08:00
const intl = useIntl()
const {
classes,
onSelectAllClick,
order,
orderBy,
numSelected,
rowCount,
onRequestSort,
} = props
const createSortHandler = (property: keyof MapInfo) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property)
}
const headCells: HeadCell[] = [
{
id: 'title',
numeric: false,
label: intl.formatMessage({ id: 'map.name', defaultMessage: 'Name' }),
},
{ id: 'labels', numeric: false },
{
id: 'createdBy',
numeric: false,
label: intl.formatMessage({ id: 'map.creator', defaultMessage: 'Creator' }),
style: { width: '70px', whiteSpace: 'nowrap' },
},
{
id: 'lastModificationTime',
numeric: true,
label: intl.formatMessage({ id: 'map.last-update', defaultMessage: 'Last Update' }),
style: { width: '70px', whiteSpace: 'nowrap' },
},
]
return (
<TableHead>
<TableRow>
<TableCell
padding="checkbox"
key="select"
style={{ width: '20px' }}
className={classes.headerCell}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
size="small"
inputProps={{ 'aria-label': 'select all desserts' }}
/>
</TableCell>
<TableCell
padding="checkbox"
key="starred"
className={classes.headerCell}
></TableCell>
{headCells.map((headCell) => {
return (
<TableCell
key={headCell.id}
sortDirection={orderBy === headCell.id ? order : false}
style={headCell.style}
className={classes.headerCell}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id && (
<span className={classes.visuallyHidden}>
{order === 'desc'
? 'sorted descending'
: 'sorted ascending'}
</span>
)}
</TableSortLabel>
</TableCell>
)
})}
<TableCell
padding="checkbox"
key="action"
className={classes.headerCell}
></TableCell>
</TableRow>
</TableHead>
)
2021-01-25 10:39:53 -08:00
}
type ActionPanelState = {
2021-02-22 22:37:29 -08:00
el: HTMLElement | undefined
mapId: number
2021-01-25 10:39:53 -08:00
}
2021-01-31 18:04:50 -08:00
interface MapsListProps {
2021-02-22 22:37:29 -08:00
filter: Filter
2021-01-31 18:04:50 -08:00
}
const mapsFilter = (filter: Filter, search: string): ((mapInfo: MapInfo) => boolean) => {
2021-02-22 22:37:29 -08:00
return (mapInfo: MapInfo) => {
// Check for filter condition
let result = false
switch (filter.type) {
case 'all':
result = true
break
case 'starred':
result = mapInfo.starred
break
case 'owned':
result = mapInfo.role == 'owner'
break
case 'shared':
result = mapInfo.role != 'owner'
break
case 'label':
result =
!mapInfo.labels || mapInfo.labels.includes((filter as LabelFilter).label.id)
break
case 'public':
result = mapInfo.isPublic
break
default:
result = false
}
2021-01-31 18:04:50 -08:00
2021-02-22 22:37:29 -08:00
// Does it match search filter criteria...
if (search && result) {
result = mapInfo.title.toLowerCase().indexOf(search.toLowerCase()) != -1
}
2021-01-31 18:04:50 -08:00
2021-02-22 22:37:29 -08:00
return result
}
2021-01-31 18:04:50 -08:00
}
2021-02-19 17:37:55 -08:00
export const MapsList = (props: MapsListProps): React.ReactElement => {
2021-02-22 22:37:29 -08:00
const classes = useStyles()
const [order, setOrder] = React.useState<Order>('asc')
const [filter, setFilter] = React.useState<Filter>({ type: 'all' })
const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('lastModificationTime')
const [selected, setSelected] = React.useState<number[]>([])
const [searchCondition, setSearchCondition] = React.useState<string>('')
const [page, setPage] = React.useState(0)
const [rowsPerPage, setRowsPerPage] = React.useState(10)
const client: Client = useSelector(activeInstance)
const intl = useIntl()
const queryClient = useQueryClient()
// Configure locale ...
const account = fetchAccount()
if (account) {
dayjs.locale(account.locale.code)
2021-01-25 10:39:53 -08:00
}
2021-02-22 22:37:29 -08:00
useEffect(() => {
setSelected([])
setPage(0)
setFilter(props.filter)
}, [props.filter.type, (props.filter as LabelFilter).label])
const { isLoading, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', () => {
return client.fetchAllMaps()
})
const mapsInfo: MapInfo[] = data ? data.filter(mapsFilter(filter, searchCondition)) : []
const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(
undefined
)
type ActiveDialog = {
actionType: ActionType
mapsId: number[]
}
2021-01-25 10:39:53 -08:00
2021-02-22 22:37:29 -08:00
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)
}
2021-01-25 10:39:53 -08:00
2021-02-22 22:37:29 -08:00
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>): void => {
if (event.target.checked) {
const newSelecteds = mapsInfo.map((n) => n.id)
setSelected(newSelecteds)
return
}
setSelected([])
}
2021-01-25 10:39:53 -08:00
2021-02-22 22:37:29 -08:00
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)
)
2021-01-25 10:39:53 -08:00
}
2021-02-22 22:37:29 -08:00
setSelected(newSelected)
2021-01-27 17:27:19 -08:00
}
2021-02-22 22:37:29 -08:00
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage)
}
2021-01-27 17:27:19 -08:00
2021-02-22 22:37:29 -08:00
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10))
setPage(0)
}
2021-01-25 10:39:53 -08:00
2021-02-22 22:37:29 -08:00
const handleActionClick = (mapId: number): ((event) => void) => {
return (event): void => {
setActiveRowAction({
mapId: mapId,
el: event.currentTarget,
})
event.stopPropagation()
}
2021-01-25 10:39:53 -08:00
}
2021-01-27 17:27:19 -08:00
2021-02-22 22:37:29 -08:00
const starredMultation = useMutation<void, ErrorInfo, number>(
(id: number) => {
const map = mapsInfo.find((m) => m.id == id)
return client.updateStarred(id, !map?.starred)
},
{
onSuccess: () => {
queryClient.invalidateQueries('maps')
},
onError: () => {
// setError(error);
},
}
)
2021-02-02 10:54:37 -08:00
2021-02-22 22:37:29 -08:00
const handleStarred = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: number) => {
event.stopPropagation()
starredMultation.mutate(id)
}
const handleActionMenuClose = (action: ActionType): void => {
if (action) {
const mapId = activeRowAction?.mapId
2021-02-02 10:54:37 -08:00
2021-02-22 22:37:29 -08:00
setActiveDialog({
actionType: action as ActionType,
mapsId: [mapId] as number[],
})
}
setActiveRowAction(undefined)
}
2021-02-02 10:54:37 -08:00
2021-02-22 22:37:29 -08:00
const handleOnSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchCondition(e.target.value)
}
2021-02-02 10:54:37 -08:00
2021-02-22 22:37:29 -08:00
const handleDeleteClick = () => {
setActiveDialog({
actionType: 'delete',
mapsId: selected,
})
}
const isSelected = (id: number) => selected.indexOf(id) !== -1
return (
<div className={classes.root}>
<ActionChooser
anchor={activeRowAction?.el}
onClose={handleActionMenuClose}
mapId={activeRowAction?.mapId}
/>
2021-02-02 10:54:37 -08:00
2021-02-22 22:37:29 -08:00
<Paper className={classes.paper} elevation={0}>
<Toolbar className={classes.toolbar} variant="dense">
<div className={classes.toolbarActions}>
{selected.length > 0 && (
<Tooltip arrow={true} title="Delete selected">
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
disableElevation={true}
onClick={handleDeleteClick}
startIcon={<DeleteOutlined />}
>
<FormattedMessage id="action.delete" defaultMessage="Delete" />
</Button>
2021-02-02 10:54:37 -08:00
</Tooltip>
2021-02-22 22:37:29 -08:00
)}
{selected.length > 0 && (
<Tooltip arrow={true} 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>
</Tooltip>
)}
</div>
<div className={classes.toolbarListActions}>
<TablePagination
style={{ float: 'right', border: '0', paddingBottom: '5px' }}
count={mapsInfo.length}
rowsPerPageOptions={[]}
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' }}
onChange={handleOnSearchChange}
/>
</div>
</div>
</Toolbar>
<TableContainer>
<Table className={classes.table} size="small" stickyHeader>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={mapsInfo.length}
/>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={6}>Loading ...</TableCell>
</TableRow>
) : mapsInfo.length == 0 ? (
<TableRow>
<TableCell colSpan={6} style={{ textAlign: 'center' }}>
<FormattedMessage
id="maps.empty-result"
defaultMessage="No matching record found with the current filter criteria."
/>
</TableCell>
</TableRow>
) : (
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) => 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 arrow={true} 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
arrow={true}
title="Open for edition"
placement="bottom-start"
>
<Link
href={`/c/maps/${row.id}/edit`}
color="textPrimary"
underline="always"
onClick={(e) => e.stopPropagation()}
>
{row.title}
</Link>
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
{row.labels}
</TableCell>
<TableCell className={classes.bodyCell}>
{row.createdBy}
</TableCell>
<TableCell className={classes.bodyCell}>
<Tooltip
arrow={true}
title={`Modified by ${
row.lastModificationBy
} on ${dayjs(
row.lastModificationTime
).format('lll')}`}
placement="bottom-start"
>
<span>
{dayjs(
row.lastModificationTime
).fromNow()}
</span>
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'map.more-actions',
defaultMessage: 'More Actions',
})}
>
<IconButton
aria-label="Others"
size="small"
onClick={handleActionClick(row.id)}
>
<MoreHorizIcon color="action" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
<ActionDispatcher
action={activeDialog?.actionType}
onClose={() => setActiveDialog(undefined)}
mapsId={activeDialog ? activeDialog.mapsId : []}
/>
</div>
)
2021-02-04 23:05:46 -08:00
}