diff --git a/src/actions/item.js b/src/actions/item.js index d059dba73..0809a274d 100644 --- a/src/actions/item.js +++ b/src/actions/item.js @@ -22,6 +22,8 @@ import { FLAG_SETTING_ITEM, FLAG_EDITING_ITEM, GET_SHARED_ITEMS_SUCCESS, + FLAG_DELETING_ITEMS, + DELETE_ITEMS_SUCCESS, } from '../types/item'; import { getParentsIdsFromPath } from '../utils/item'; import { createFlag } from './utils'; @@ -129,13 +131,35 @@ export const createItem = (props) => async (dispatch) => { } }; -export const deleteItem = (item) => async (dispatch) => { +export const deleteItem = (itemId) => async (dispatch) => { try { dispatch(createFlag(FLAG_DELETING_ITEM, true)); - await Api.deleteItem(item.id); + await Api.deleteItem(itemId); dispatch({ type: DELETE_ITEM_SUCCESS, - payload: item, + payload: { id: itemId }, + }); + } catch (e) { + console.error(e); + } finally { + dispatch(createFlag(FLAG_DELETING_ITEM, false)); + } +}; + +export const deleteItems = (itemIds) => async (dispatch) => { + try { + dispatch(createFlag(FLAG_DELETING_ITEMS, true)); + + // choose corresponding call depending on number of items + if (itemIds.length === 1) { + await Api.deleteItem(itemIds); + } else { + await Api.deleteItems(itemIds); + } + + dispatch({ + type: DELETE_ITEMS_SUCCESS, + payload: itemIds, }); } catch (e) { console.error(e); diff --git a/src/api/item.js b/src/api/item.js index 72ef9edb6..71287df0a 100644 --- a/src/api/item.js +++ b/src/api/item.js @@ -2,6 +2,7 @@ import { API_HOST, ROOT_ID } from '../config/constants'; import { buildCopyItemRoute, buildDeleteItemRoute, + buildDeleteItemsRoute, buildEditItemRoute, buildGetChildrenRoute, buildGetItemRoute, @@ -87,6 +88,20 @@ export const deleteItem = async (id) => { return res.json(); }; +export const deleteItems = async (ids) => { + const res = await fetch( + `${API_HOST}/${buildDeleteItemsRoute(ids)}`, + DEFAULT_DELETE, + ); + + if (!res.ok) { + throw new Error((await res.json()).message); + } + await CacheOperations.deleteItems(ids); + + return res.json(); +}; + // payload = {name, type, description, extra} // querystring = {parentId} export const editItem = async (item) => { diff --git a/src/api/routes.js b/src/api/routes.js index ea7ec8d5b..1959c24e7 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -8,6 +8,8 @@ export const buildPostItemRoute = (parentId) => { return url; }; export const buildDeleteItemRoute = (id) => `items/${id}`; +export const buildDeleteItemsRoute = (ids) => + `items?${ids.map((id) => `id=${id}`).join('&')}`; export const buildGetChildrenRoute = (id) => `items/${id}/children`; export const buildGetItemRoute = (id) => `items/${id}`; export const buildMoveItemRoute = (id) => `items/${id}/move`; diff --git a/src/components/Root.js b/src/components/Root.js index 298660e51..fa2bfc616 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -11,6 +11,7 @@ const theme = createMuiTheme({ primary: { main: '#5050d2', }, + secondary: { main: '#ffffff' }, }, }); diff --git a/src/components/common/DeleteButton.js b/src/components/common/DeleteButton.js new file mode 100644 index 000000000..a0991871e --- /dev/null +++ b/src/components/common/DeleteButton.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import Tooltip from '@material-ui/core/Tooltip'; +import PropTypes from 'prop-types'; +import IconButton from '@material-ui/core/IconButton'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { deleteItems } from '../../actions/item'; +import { ITEM_DELETE_BUTTON_CLASS } from '../../config/selectors'; + +const DeleteButton = ({ itemIds, dispatchDeleteItems, color }) => { + const { t } = useTranslation(); + + return ( + + dispatchDeleteItems(itemIds)} + > + + + + ); +}; + +DeleteButton.propTypes = { + itemIds: PropTypes.string.isRequired, + dispatchDeleteItems: PropTypes.func.isRequired, + color: PropTypes.string, +}; + +DeleteButton.defaultProps = { + color: '', +}; + +const mapDispatchToProps = { + dispatchDeleteItems: deleteItems, +}; + +const ConnectedComponent = connect(null, mapDispatchToProps)(DeleteButton); + +export default ConnectedComponent; diff --git a/src/components/common/EditButton.js b/src/components/common/EditButton.js new file mode 100644 index 000000000..71dc89bd9 --- /dev/null +++ b/src/components/common/EditButton.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import IconButton from '@material-ui/core/IconButton'; +import EditIcon from '@material-ui/icons/Edit'; +import { useTranslation } from 'react-i18next'; +import Tooltip from '@material-ui/core/Tooltip'; +import { ITEM_MENU_EDIT_BUTTON_CLASS } from '../../config/selectors'; +import { setEditModalSettings } from '../../actions/layout'; + +const Item = ({ itemId, dispatchSetEditModalSettings }) => { + const { t } = useTranslation(); + + const handleEdit = () => { + dispatchSetEditModalSettings({ open: true, itemId }); + }; + + return ( + + + + + + ); +}; + +Item.propTypes = { + itemId: PropTypes.string.isRequired, + dispatchSetEditModalSettings: PropTypes.func.isRequired, +}; + +const mapDispatchToProps = { + dispatchSetEditModalSettings: setEditModalSettings, +}; + +const ConnectedComponent = connect(null, mapDispatchToProps)(Item); + +export default ConnectedComponent; diff --git a/src/components/common/ShareButton.js b/src/components/common/ShareButton.js new file mode 100644 index 000000000..b91e6c03c --- /dev/null +++ b/src/components/common/ShareButton.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import IconButton from '@material-ui/core/IconButton'; +import ShareIcon from '@material-ui/icons/Share'; +import { useTranslation } from 'react-i18next'; +import Tooltip from '@material-ui/core/Tooltip'; +import { ITEM_MENU_SHARE_BUTTON_CLASS } from '../../config/selectors'; +import { setShareModalSettings } from '../../actions/layout'; + +const Item = ({ itemId, dispatchSetShareModalSettings }) => { + const { t } = useTranslation(); + + const handleShare = () => { + dispatchSetShareModalSettings({ open: true, itemId }); + }; + + return ( + + + + + + ); +}; + +Item.propTypes = { + itemId: PropTypes.string.isRequired, + dispatchSetShareModalSettings: PropTypes.func.isRequired, +}; + +const mapDispatchToProps = { + dispatchSetShareModalSettings: setShareModalSettings, +}; + +const ConnectedComponent = connect(null, mapDispatchToProps)(Item); + +export default ConnectedComponent; diff --git a/src/components/main/CustomCardHeader.js b/src/components/main/CustomCardHeader.js index c2210aa2e..82baced4a 100644 --- a/src/components/main/CustomCardHeader.js +++ b/src/components/main/CustomCardHeader.js @@ -59,7 +59,7 @@ const CustomCardHeader = ({ item }) => { - + ); }; diff --git a/src/components/main/EmptyItem.js b/src/components/main/EmptyItem.js new file mode 100644 index 000000000..7e761266c --- /dev/null +++ b/src/components/main/EmptyItem.js @@ -0,0 +1,21 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import { useTranslation } from 'react-i18next'; +import { ITEMS_GRID_NO_ITEM_ID } from '../../config/selectors'; + +const EmptyItem = () => { + const { t } = useTranslation(); + + return ( + + {t('This item is empty.')} + + ); +}; + +export default EmptyItem; diff --git a/src/components/main/Home.js b/src/components/main/Home.js index f40b8c260..946e6060d 100644 --- a/src/components/main/Home.js +++ b/src/components/main/Home.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { List } from 'immutable'; import PropTypes from 'prop-types'; import Divider from '@material-ui/core/Divider'; import { connect } from 'react-redux'; @@ -15,8 +16,8 @@ class Home extends Component { params: PropTypes.shape({ itemId: PropTypes.string }).isRequired, }).isRequired, activity: PropTypes.bool.isRequired, - ownItems: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - sharedItems: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + ownItems: PropTypes.instanceOf(List).isRequired, + sharedItems: PropTypes.instanceOf(List).isRequired, t: PropTypes.func.isRequired, dispatchGetSharedItems: PropTypes.func.isRequired, }; @@ -62,7 +63,7 @@ class Home extends Component { } const mapStateToProps = ({ item }) => ({ - activity: Object.values(item.get('activity').toJS()).flat().length, + activity: Boolean(Object.values(item.get('activity').toJS()).flat().length), ownItems: item.get('own'), sharedItems: item.get('shared'), }); diff --git a/src/components/main/Item.js b/src/components/main/Item.js index d3cd6470e..95770808e 100644 --- a/src/components/main/Item.js +++ b/src/components/main/Item.js @@ -1,5 +1,4 @@ import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import truncate from 'lodash.truncate'; @@ -7,19 +6,16 @@ import Card from '@material-ui/core/Card'; import CardMedia from '@material-ui/core/CardMedia'; import CardContent from '@material-ui/core/CardContent'; import CardActions from '@material-ui/core/CardActions'; -import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; -import DeleteIcon from '@material-ui/icons/Delete'; import CustomCardHeader from './CustomCardHeader'; -import { deleteItem } from '../../actions/item'; import { DEFAULT_IMAGE_SRC, DESCRIPTION_MAX_LENGTH, } from '../../config/constants'; -import { - buildItemCard, - ITEM_DELETE_BUTTON_CLASS, -} from '../../config/selectors'; +import { buildItemCard } from '../../config/selectors'; +import EditButton from '../common/EditButton'; +import ShareButton from '../common/ShareButton'; +import DeleteButton from '../common/DeleteButton'; const useStyles = makeStyles(() => ({ root: { @@ -31,7 +27,7 @@ const useStyles = makeStyles(() => ({ }, })); -const Item = ({ item, dispatchDeleteItem }) => { +const Item = ({ item }) => { const classes = useStyles(); const { id, name, description, extra } = item; @@ -49,13 +45,9 @@ const Item = ({ item, dispatchDeleteItem }) => { - dispatchDeleteItem(item)} - > - - + + + ); @@ -72,13 +64,6 @@ Item.propTypes = { image: PropTypes.string.isRequired, }).isRequired, }).isRequired, - dispatchDeleteItem: PropTypes.func.isRequired, -}; - -const mapDispatchToProps = { - dispatchDeleteItem: deleteItem, }; -const ConnectedComponent = connect(null, mapDispatchToProps)(Item); - -export default ConnectedComponent; +export default Item; diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js index a47f7923c..5ebddd725 100644 --- a/src/components/main/ItemMenu.js +++ b/src/components/main/ItemMenu.js @@ -9,25 +9,19 @@ import MoreVertIcon from '@material-ui/icons/MoreVert'; import { setMoveModalSettings, setCopyModalSettings, - setEditModalSettings, - setShareModalSettings, } from '../../actions/layout'; import { buildItemMenu, ITEM_MENU_BUTTON_CLASS, ITEM_MENU_COPY_BUTTON_CLASS, - ITEM_MENU_EDIT_BUTTON_CLASS, ITEM_MENU_MOVE_BUTTON_CLASS, - ITEM_MENU_SHARE_BUTTON_CLASS, } from '../../config/selectors'; import { editItem } from '../../actions/item'; const ItemMenu = ({ - item, + itemId, dispatchSetMoveModalSettings, dispatchSetCopyModalSettings, - dispatchSetEditModalSettings, - dispatchSetShareModalSettings, }) => { const [anchorEl, setAnchorEl] = React.useState(null); const { t } = useTranslation(); @@ -41,22 +35,12 @@ const ItemMenu = ({ }; const handleMove = () => { - dispatchSetMoveModalSettings({ open: true, itemId: item.id }); + dispatchSetMoveModalSettings({ open: true, itemId }); handleClose(); }; const handleCopy = () => { - dispatchSetCopyModalSettings({ open: true, itemId: item.id }); - handleClose(); - }; - - const handleEdit = () => { - dispatchSetEditModalSettings({ open: true, itemId: item.id }); - handleClose(); - }; - - const handleShare = () => { - dispatchSetShareModalSettings({ open: true, itemId: item.id }); + dispatchSetCopyModalSettings({ open: true, itemId }); handleClose(); }; @@ -66,47 +50,32 @@ const ItemMenu = ({ - - {t('Edit')} - {t('Move')} {t('Copy')} - - {t('Share')} - ); }; ItemMenu.propTypes = { - dispatchSetEditModalSettings: PropTypes.func.isRequired, - item: PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired, + itemId: PropTypes.string.isRequired, dispatchSetMoveModalSettings: PropTypes.func.isRequired, dispatchSetCopyModalSettings: PropTypes.func.isRequired, - dispatchSetShareModalSettings: PropTypes.func.isRequired, }; const mapDispatchToProps = { dispatchSetMoveModalSettings: setMoveModalSettings, dispatchSetCopyModalSettings: setCopyModalSettings, - dispatchSetEditModalSettings: setEditModalSettings, - dispatchSetShareModalSettings: setShareModalSettings, dispatchEditItem: editItem, }; diff --git a/src/components/main/Items.js b/src/components/main/Items.js index fbfe61a5a..6f9a69e0f 100644 --- a/src/components/main/Items.js +++ b/src/components/main/Items.js @@ -1,27 +1,17 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import Typography from '@material-ui/core/Typography'; +import { List } from 'immutable'; import { withRouter } from 'react-router'; -import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; import { MODES } from '../../config/constants'; import ItemsTable from './ItemsTable'; -import NewItemButton from './NewItemButton'; import ItemsGrid from './ItemsGrid'; -const styles = (theme) => ({ - title: { - display: 'flex', - alignItems: 'center', - marginBottom: theme.spacing(1), - }, -}); - // eslint-disable-next-line react/prefer-stateless-function class Items extends Component { static propTypes = { - items: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + items: PropTypes.instanceOf(List).isRequired, mode: PropTypes.oneOf(Object.values(MODES)).isRequired, title: PropTypes.string.isRequired, classes: PropTypes.shape({ @@ -30,15 +20,9 @@ class Items extends Component { }; render() { - const { items, mode, title, classes } = this.props; + const { items, mode, title } = this.props; return mode === MODES.CARD ? ( - <> - - {title} - - - - + ) : ( ); @@ -50,6 +34,5 @@ const mapStateToProps = ({ layout }) => ({ }); const ConnectedComponent = connect(mapStateToProps)(Items); -const StyledComponent = withStyles(styles)(ConnectedComponent); -const TranslatedComponent = withTranslation()(StyledComponent); +const TranslatedComponent = withTranslation()(ConnectedComponent); export default withRouter(TranslatedComponent); diff --git a/src/components/main/ItemsGrid.js b/src/components/main/ItemsGrid.js index 713306d9d..b4cffc426 100644 --- a/src/components/main/ItemsGrid.js +++ b/src/components/main/ItemsGrid.js @@ -2,35 +2,37 @@ import React, { Component } from 'react'; import { List } from 'immutable'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; -import { withTranslation } from 'react-i18next'; -import Grid from '@material-ui/core/Grid'; +import { withStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import NewItemButton from './NewItemButton'; import Item from './Item'; -import { ITEMS_GRID_NO_ITEM_ID } from '../../config/selectors'; +import EmptyItem from './EmptyItem'; +const styles = (theme) => ({ + title: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), + }, +}); class ItemsGrid extends Component { static propTypes = { items: PropTypes.instanceOf(List).isRequired, match: PropTypes.shape({ params: PropTypes.shape({ itemId: PropTypes.string }).isRequired, }).isRequired, - t: PropTypes.func.isRequired, + classes: PropTypes.shape({ + title: PropTypes.string.isRequired, + }).isRequired, + title: PropTypes.string.isRequired, }; renderItems = () => { - const { items, t } = this.props; + const { items } = this.props; if (!items || !items.size) { - return ( - - {t('No Item Here')} - - ); + return ; } return items.map((item) => ( @@ -41,8 +43,13 @@ class ItemsGrid extends Component { }; render() { + const { classes, title } = this.props; return ( <> + + {title} + + {this.renderItems()} @@ -50,5 +57,5 @@ class ItemsGrid extends Component { ); } } -const TranslatedComponent = withTranslation()(ItemsGrid); -export default withRouter(TranslatedComponent); +const StyledComponent = withStyles(styles)(ItemsGrid); +export default withRouter(StyledComponent); diff --git a/src/components/main/ItemsTable.js b/src/components/main/ItemsTable.js index 3d5237af4..73160e417 100644 --- a/src/components/main/ItemsTable.js +++ b/src/components/main/ItemsTable.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { List } from 'immutable'; import clsx from 'clsx'; @@ -17,18 +17,20 @@ 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 ItemMenu from './ItemMenu'; import { buildItemPath } from '../../config/paths'; import { ORDERING, TABLE_MIN_WIDTH, ROWS_PER_PAGE_OPTIONS, + ITEM_DATA_TYPES, } from '../../config/constants'; -import { getComparator, stableSort } from '../../utils/table'; +import { getComparator, stableSort, getRowsForPage } from '../../utils/table'; import { formatDate } from '../../utils/date'; import NewItemButton from './NewItemButton'; +import EditButton from '../common/EditButton'; +import ShareButton from '../common/ShareButton'; +import DeleteButton from '../common/DeleteButton'; const EnhancedTableHead = (props) => { const { @@ -61,8 +63,7 @@ const EnhancedTableHead = (props) => { {headCells.map((headCell) => ( ({ display: 'flex', alignItems: 'center', }, + highlight: { + background: theme.palette.primary.main, + color: 'white', + }, })); const EnhancedTableToolbar = (props) => { const classes = useToolbarStyles(); const { t } = useTranslation(); - const { numSelected, tableTitle } = props; + const { numSelected, tableTitle, selected } = props; return ( { )} {numSelected > 0 ? ( - - - - - + ) : null} ); @@ -157,6 +158,7 @@ const EnhancedTableToolbar = (props) => { EnhancedTableToolbar.propTypes = { numSelected: PropTypes.number.isRequired, tableTitle: PropTypes.string, + selected: PropTypes.arrayOf(PropTypes.shape({}).isRequired).isRequired, }; EnhancedTableToolbar.defaultProps = { @@ -188,52 +190,97 @@ const useStyles = makeStyles((theme) => ({ selected: { backgroundColor: `${lighten(theme.palette.primary.main, 0.85)} !important`, }, + hover: { + cursor: 'pointer', + }, })); const ItemsTable = ({ items: rows, tableTitle }) => { const classes = useStyles(); const { t } = useTranslation(); const { push } = useHistory(); - const [order, setOrder] = React.useState(ORDERING.ASC); + const [order, setOrder] = React.useState(ORDERING.DESC); const [orderBy, setOrderBy] = React.useState('updatedAt'); const [selected, setSelected] = React.useState([]); const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [rowsPerPage, setRowsPerPage] = React.useState( + ROWS_PER_PAGE_OPTIONS[0], + ); + + useEffect(() => { + // remove deleted rows from selection + const newSelected = selected.filter( + (id) => rows.findIndex(({ id: thisId }) => thisId === id) >= 0, + ); + if (newSelected.length !== selected.length) { + setSelected(newSelected); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rows]); const headCells = [ { id: 'name', numeric: false, - disablePadding: true, label: t('Name'), + align: 'left', }, { id: 'type', numeric: false, - disablePadding: true, label: t('Type'), + align: 'right', }, { id: 'createdAt', numeric: false, - disablePadding: true, label: t('Created At'), + align: 'right', + type: ITEM_DATA_TYPES.DATE, }, { id: 'updatedAt', numeric: false, - disablePadding: true, label: t('Updated At'), + align: 'right', + type: ITEM_DATA_TYPES.DATE, + }, + { + id: 'actions', + numeric: false, + label: t('Actions'), + align: 'right', }, ]; - if (!rows.size) { - return ( - - {t('No Item Here')} - - ); - } + // display empty rows to maintain the table height + const emptyRows = + rowsPerPage - Math.min(rowsPerPage, rows.size - page * rowsPerPage); + + // order and select rows to display given the current page and the number of entries displayed + const rowsToDisplay = getRowsForPage( + stableSort(rows, getComparator(order, orderBy)), + { page, rowsPerPage }, + ); + + // transform rows' information into displayable information + const mappedRows = rowsToDisplay.map( + ({ id, name, updatedAt, createdAt, type }) => ({ + id, + name, + type, + updatedAt, + createdAt, + actions: ( + <> + + + + + + ), + }), + ); const handleRequestSort = (event, property) => { const isAsc = orderBy === property && order === ORDERING.ASC; @@ -242,34 +289,34 @@ const ItemsTable = ({ items: rows, tableTitle }) => { }; const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = rows.map((n) => n.id).toJS(); - setSelected(newSelecteds); - return; + const checked = + JSON.parse(event.target.dataset.indeterminate) || !event.target.checked; + if (!checked) { + const newSelecteds = mappedRows.map((n) => n.id).toJS(); + return setSelected(newSelecteds); } - setSelected([]); + return setSelected([]); }; - const handleClick = (event, id) => { - 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), - ); - } + const removeItemsFromSelected = (items) => { + const newSelected = selected.filter((id) => !items.includes(id)); + setSelected(newSelected); + }; + const addItemsInSelected = (items) => { + const newSelected = selected.concat(items); setSelected(newSelected); }; + const handleClick = (event, id) => { + const checked = selected.indexOf(id) !== -1; + if (checked) { + removeItemsFromSelected([id]); + } else { + addItemsInSelected([id]); + } + }; + const handleChangePage = (event, newPage) => { setPage(newPage); }; @@ -283,19 +330,17 @@ const ItemsTable = ({ items: rows, tableTitle }) => { push(buildItemPath(id)); }; - const isSelected = (id) => selected.indexOf(id) !== -1; - - const emptyRows = - rowsPerPage - Math.min(rowsPerPage, rows.size - page * rowsPerPage); + // format entry data given type + const formatRowValue = ({ value, type }) => { + switch (type) { + case ITEM_DATA_TYPES.DATE: + return formatDate(value); + default: + return value; + } + }; - // transform rows' information into displayable information - const mappedRows = rows.map(({ id, name, updatedAt, createdAt, type }) => ({ - id, - name, - type, - updatedAt: formatDate(updatedAt), - createdAt: formatDate(createdAt), - })); + const isSelected = (id) => selected.indexOf(id) !== -1; return (
@@ -303,6 +348,7 @@ const ItemsTable = ({ items: rows, tableTitle }) => { { headCells={headCells} /> - {stableSort(mappedRows, getComparator(order, orderBy)) - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row, index) => { - const isItemSelected = isSelected(row.id); - const labelId = `enhanced-table-checkbox-${index}`; + {mappedRows.map((row, index) => { + const isItemSelected = isSelected(row.id); + const labelId = `enhanced-table-checkbox-${index}`; - return ( - - - handleClick(event, row.id)} - color="primary" - /> - - { - // does not render name - headCells.map(({ id: field }) => ( - handleRowOnClick(row.id)} - > - {row[field]} - - )) - } - - ); - })} + return ( + + + handleClick(event, row.id)} + color="primary" + /> + + { + // does not render name + headCells.map(({ id: field, align, type }, idx) => ( + { + // do not navigate when clicking on actions + const shouldNavigate = idx !== headCells.length - 1; + if (shouldNavigate) { + handleRowOnClick(row.id); + } + }} + > + {formatRowValue({ value: row[field], type })} + + )) + } + + ); + })} {emptyRows > 0 && ( diff --git a/src/components/main/ShareItemModal.js b/src/components/main/ShareItemModal.js index e34ef7b40..1bee4742d 100644 --- a/src/components/main/ShareItemModal.js +++ b/src/components/main/ShareItemModal.js @@ -88,7 +88,11 @@ const ShareItemModal = ({ label={t('Permission')} > {Object.values(PERMISSION_LEVELS).map((p) => ( - + {p} ))} diff --git a/src/config/cache.js b/src/config/cache.js index eeaceec32..f16128a91 100644 --- a/src/config/cache.js +++ b/src/config/cache.js @@ -51,6 +51,10 @@ export const deleteItem = async (id) => { cache.items.delete(id); }; +export const deleteItems = async (ids) => { + ids.forEach((id) => deleteItem(id)); +}; + export const moveItem = async ({ id, to, from }) => { await cache.items.update(id, { dirty: true }); if (to && to !== ROOT_ID) { diff --git a/src/config/constants.js b/src/config/constants.js index 818b782ab..8f49665b0 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -46,4 +46,8 @@ export const ORDERING = { export const TABLE_MIN_WIDTH = 750; -export const ROWS_PER_PAGE_OPTIONS = [5, 10, 25]; +export const ROWS_PER_PAGE_OPTIONS = [10, 25]; + +export const ITEM_DATA_TYPES = { + DATE: 'date', +}; diff --git a/src/reducers/item.js b/src/reducers/item.js index 23bea23ad..8bddfc003 100644 --- a/src/reducers/item.js +++ b/src/reducers/item.js @@ -23,6 +23,7 @@ import { EDIT_ITEM_SUCCESS, FLAG_EDITING_ITEM, GET_SHARED_ITEMS_SUCCESS, + DELETE_ITEMS_SUCCESS, } from '../types/item'; const DEFAULT_ITEM = Map({ @@ -116,6 +117,21 @@ export default (state = INITIAL_STATE, { type, payload }) => { } return state.updateIn(['item', 'children'], updateInList(payload)); } + case DELETE_ITEMS_SUCCESS: { + let newState = state; + const from = newState.getIn(['item', 'id']); + for (const id of payload) { + // delete item in children or in root items + if (!from) { + newState = newState.update('own', removeFromList({ id })); + } + newState = newState.updateIn( + ['item', 'children'], + removeFromList({ id }), + ); + } + return newState; + } case DELETE_ITEM_SUCCESS: case MOVE_ITEM_SUCCESS: { const from = state.getIn(['item', 'id']); diff --git a/src/types/item.js b/src/types/item.js index 1c29edc94..440022252 100644 --- a/src/types/item.js +++ b/src/types/item.js @@ -21,3 +21,5 @@ export const EDIT_ITEM_SUCCESS = 'EDIT_ITEM_SUCCESS'; export const FLAG_EDITING_ITEM = 'FLAG_EDITING_ITEM'; export const SET_SELECTED_ITEM_SUCCESS = 'SET_SELECTED_ITEM_SUCCESS'; export const GET_SHARED_ITEMS_SUCCESS = 'GET_SHARED_ITEMS_SUCCESS'; +export const FLAG_DELETING_ITEMS = 'FLAG_DELETING_ITEMS'; +export const DELETE_ITEMS_SUCCESS = 'DELETE_ITEMS_SUCCESS'; diff --git a/src/utils/date.js b/src/utils/date.js index 7f46c51a4..919898e6c 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -5,5 +5,5 @@ export const formatDate = (d) => { const datetime = new Date(d); const time = datetime.toLocaleTimeString(DEFAULT_LOCALE); const date = datetime.toLocaleDateString(DEFAULT_LOCALE); - return `${time} ${date}`; + return `${date} ${time}`; }; diff --git a/src/utils/table.js b/src/utils/table.js index 3ce2eb832..5cb31b502 100644 --- a/src/utils/table.js +++ b/src/utils/table.js @@ -24,3 +24,6 @@ export const stableSort = (array, comparator) => { }); return stabilizedThis.map((el) => el[0]); }; + +export const getRowsForPage = (table, { page, rowsPerPage }) => + table.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);