2021-02-07 18:59:45 +01:00
import React , { useEffect , CSSProperties } 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 { 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-13 04:03:47 +01:00
import { ErrorInfo , MapInfo } from '../../../classes/client' ;
import Client from '../../../classes/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-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 } ,
2021-02-07 02:47:34 +01:00
{ 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' } }
2021-02-05 22:15:36 +01:00
] ;
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-02-07 02:47:34 +01:00
const [ orderBy , setOrderBy ] = React . useState < keyof MapInfo > ( 'lastModificationTime' ) ;
2021-01-25 19:39:53 +01:00
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-07 07:51:04 +01:00
const queryClient = useQueryClient ( ) ;
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
2021-02-07 18:59:45 +01:00
const { isLoading , 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 ;
2021-02-07 07:51:04 +01:00
mapsId : number [ ] ;
2021-01-25 19:39:53 +01: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 ) ;
} ;
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 starredMultation = useMutation < void , ErrorInfo , number > ( ( id : number ) = > {
2021-02-04 23:34:13 +01:00
const map = mapsInfo . find ( m = > m . id == id ) ;
2021-02-07 18:59:45 +01:00
return client . updateStarred ( id , ! Boolean ( map ? . starred ) ) ;
2021-01-28 02:27:19 +01:00
} ,
{
onSuccess : ( ) = > {
queryClient . invalidateQueries ( 'maps' ) ;
} ,
2021-02-07 18:59:45 +01:00
onError : ( ) = > {
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 ,
2021-02-07 07:51:04 +01:00
mapsId : [ mapId ] as number [ ]
2021-01-25 19:39:53 +01:00
} ) ;
}
2021-01-28 02:27:19 +01:00
setActiveRowAction ( undefined ) ;
2021-01-25 19:39:53 +01:00
} ;
2021-02-07 02:47:34 +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-07 18:59:45 +01:00
const handleDeleteClick = ( ) = > {
2021-02-07 07:51:04 +01:00
setActiveDialog ( {
actionType : 'delete' ,
mapsId : selected
} ) ;
}
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-07 02:47:34 +01:00
< ActionChooser anchor = { activeRowAction ? . el } onClose = { handleActionMenuClose } mapId = { activeRowAction ? . mapId } / >
2021-02-06 17:27:37 +01:00
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 }
2021-02-07 07:51:04 +01:00
onClick = { handleDeleteClick }
startIcon = { < DeleteOutlined
/ > } >
2021-02-02 07:15:32 +01:00
< 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 } >
2021-02-07 02:47:34 +01:00
{ row . createdBy }
2021-02-02 19:54:37 +01:00
< / TableCell >
< TableCell className = { classes . bodyCell } >
2021-02-07 02:47:34 +01:00
< Tooltip title = {
` Modified by ${ row . lastModificationBy } on ${ moment ( row . lastModificationTime ) . format ( "lll" ) } `
} placement = "bottom-start" >
< span > { moment ( row . lastModificationTime ) . fromNow ( ) } < / span >
2021-02-02 19:54:37 +01:00
< / 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 >
2021-02-07 07:51:04 +01:00
< ActionDispatcher action = { activeDialog ? . actionType } onClose = { ( ) = > setActiveDialog ( undefined ) } mapsId = { activeDialog ? activeDialog . mapsId : [ ] } / >
2021-01-28 02:27:19 +01:00
< / d i v >
2021-01-25 19:39:53 +01:00
) ;
2021-02-05 08:05:46 +01:00
}