diff --git a/src/api/item.js b/src/api/item.js index 60dd9a91b..fc2b693df 100644 --- a/src/api/item.js +++ b/src/api/item.js @@ -1,6 +1,15 @@ import { API_HOST } from '../config/constants'; import { DEFAULT_DELETE, DEFAULT_GET, DEFAULT_POST } from './utils'; +// payload = {id} +export const getItem = async (id) => { + const req = await fetch(`${API_HOST}/items/${id}`, { + ...DEFAULT_GET, + headers: { 'Content-Type': 'application/json' }, + }); + return req.json(); +}; + // payload = {email} export const getOwnItems = async () => { const req = await fetch(`${API_HOST}/items/own`, { @@ -38,4 +47,19 @@ export const deleteItem = async (id) => { }; // we need this function for navigation purposes: when you click on an item, you want to see its 'immediate' children -export const fetchItemImmediateChildren = () => {}; +export const getChildren = async (id) => { + const req = await fetch(`${API_HOST}/items/${id}/children`, DEFAULT_GET); + return req.json(); +}; + +export const getItemTree = async (ownedItems) => { + // todo: use parallel promises + const items = JSON.parse(JSON.stringify(ownedItems)); + // eslint-disable-next-line no-restricted-syntax + for (const item of items) { + // eslint-disable-next-line no-await-in-loop + const children = await getChildren(item.id); + item.children = children; + } + return items; +}; diff --git a/src/components/App.js b/src/components/App.js index 8b7209937..5cc06f2df 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -10,7 +10,12 @@ import Header from './layout/Header'; import Items from './main/Items'; import items from '../data/sample'; import SignUp from './SignUp'; -import { SIGN_UP_PATH, SIGN_IN_PATH } from '../config/paths'; +import { + SIGN_UP_PATH, + SIGN_IN_PATH, + HOME_PATH, + ITEMS_PATH, +} from '../config/paths'; import SignIn from './SignIn'; import { ItemProvider } from './context/item'; @@ -29,7 +34,7 @@ function App() {
- + @@ -41,10 +46,10 @@ function App() { - - + + - +
diff --git a/src/components/context/item.js b/src/components/context/item.js index c2f643de3..f26d131c7 100644 --- a/src/components/context/item.js +++ b/src/components/context/item.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { getOwnItems, deleteItem } from '../../api/item'; +import { getOwnItems, deleteItem, getChildren, getItem } from '../../api/item'; import sampleItems from '../../data/sample'; const ItemContext = React.createContext(); @@ -42,12 +42,36 @@ class ItemProvider extends Component { }); }; + getChildren = async (id) => { + if (!id) { + return getOwnItems(); + } + return getChildren(id); + }; + + getNavigation = async (itemId) => { + if (!itemId) { + return []; + } + const navigation = []; + let currentParentId = itemId; + while (currentParentId) { + // eslint-disable-next-line no-await-in-loop + const parent = await getItem(currentParentId); + navigation.push(parent); + currentParentId = parent.parentId; + } + return navigation; + }; + buildValue = () => { const { items } = this.state; return { items, addItem: this.addItem, deleteItem: this.deleteItem, + getChildren: this.getChildren, + getNavigation: this.getNavigation, }; }; diff --git a/src/components/layout/Navigation.js b/src/components/layout/Navigation.js new file mode 100644 index 000000000..38362d89a --- /dev/null +++ b/src/components/layout/Navigation.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router'; +import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import Link from '@material-ui/core/Link'; +import { HOME_PATH } from '../../config/paths'; +import { ItemContext } from '../context/item'; + +function handleClick(event) { + event.preventDefault(); +} + +class Navigation extends Component { + static contextType = ItemContext; + + static propTypes = { + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + itemId: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }; + + state = { + navigation: [], + }; + + async componentDidMount() { + this.updateNavigation(); + } + + async componentDidUpdate({ + match: { + params: { itemId: prevItemId }, + }, + }) { + const { + match: { + params: { itemId }, + }, + } = this.props; + + if (prevItemId !== itemId) { + this.updateNavigation(); + } + } + + updateNavigation = async () => { + const { getNavigation } = this.context; + const { + match: { + params: { itemId }, + }, + } = this.props; + + this.setState({ navigation: await getNavigation(itemId) }); + }; + + goHome = (event) => { + const { + history: { push }, + } = this.props; + event.preventDefault(); + push(HOME_PATH); + }; + + render() { + const { navigation } = this.state; + return ( + + + Owned Items + + {navigation.map(({ name, id }) => ( + + {name} + + ))} + + ); + } +} + +export default withRouter(Navigation); diff --git a/src/components/main/CreateNewItem.js b/src/components/main/CreateNewItem.js index 0663f201e..40a51037f 100644 --- a/src/components/main/CreateNewItem.js +++ b/src/components/main/CreateNewItem.js @@ -2,6 +2,7 @@ import React, { useState, useContext } from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; +import { withRouter } from 'react-router'; import TextField from '@material-ui/core/TextField'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; @@ -26,7 +27,13 @@ const useStyles = makeStyles((theme) => ({ }, })); -const CreateNewItem = ({ open, handleClose }) => { +const CreateNewItem = ({ + open, + handleClose, + match: { + params: { itemId }, + }, +}) => { const classes = useStyles(); const itemContext = useContext(ItemContext); const [itemName, setItemName] = useState(''); @@ -52,7 +59,9 @@ const CreateNewItem = ({ open, handleClose }) => { const submitNewItem = async () => { const { addItem } = itemContext; + const newItem = await createItem({ + parentId: itemId, name: itemName, type: itemType, description: itemDescription, @@ -144,10 +153,16 @@ const CreateNewItem = ({ open, handleClose }) => { CreateNewItem.propTypes = { open: PropTypes.bool, handleClose: PropTypes.func.isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + itemId: PropTypes.string, + }).isRequired, + }), }; CreateNewItem.defaultProps = { open: false, + match: { params: { itemId: '' } }, }; -export default CreateNewItem; +export default withRouter(CreateNewItem); diff --git a/src/components/main/CustomCardHeader.js b/src/components/main/CustomCardHeader.js index fad354ee3..79481c4f4 100644 --- a/src/components/main/CustomCardHeader.js +++ b/src/components/main/CustomCardHeader.js @@ -4,8 +4,7 @@ import { Link } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; import Avatar from '@material-ui/core/Avatar'; import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import ItemMenu from './ItemMenu'; const useStyles = makeStyles((theme) => ({ root: { @@ -50,9 +49,7 @@ const CustomCardHeader = ({ id, creator, title, type }) => { - - - + ); }; diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js new file mode 100644 index 000000000..bcb89b999 --- /dev/null +++ b/src/components/main/ItemMenu.js @@ -0,0 +1,51 @@ +import React from 'react'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import IconButton from '@material-ui/core/IconButton'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import MoveItemModal from './MoveItemModal'; + +const ItemMenu = () => { + const [anchorEl, setAnchorEl] = React.useState(null); + const [isMoveModalOpen, setIsMoveModalOpen] = React.useState(false); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleMove = () => { + setIsMoveModalOpen(true); + handleClose(); + }; + + const onModalClose = () => { + setIsMoveModalOpen(false); + }; + + // todo: only display one modal for the whole page + + return ( +
+ + + + + Move + Some action... + + +
+ ); +}; + +export default ItemMenu; diff --git a/src/components/main/Items.js b/src/components/main/Items.js index df3100482..332a4e63c 100644 --- a/src/components/main/Items.js +++ b/src/components/main/Items.js @@ -1,5 +1,8 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router'; import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; import ItemsHeader from './ItemsHeader'; import CreateNewItemButton from './CreateNewItemButton'; import Item from './Item'; @@ -8,22 +11,82 @@ import { ItemContext } from '../context/item'; class Items extends Component { static contextType = ItemContext; + static propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + itemId: PropTypes.string, + }).isRequired, + }), + }; + + static defaultProps = { + match: { params: { itemId: '' } }, + }; + + state = { items: [] }; + + async componentDidMount() { + this.updateItems(); + } + + async componentDidUpdate({ + match: { + params: { itemId: prevItemId }, + }, + }) { + const { + match: { + params: { itemId }, + }, + } = this.props; + + if (prevItemId !== itemId) { + this.updateItems(); + } + } + + updateItems = async () => { + const { + match: { + params: { itemId }, + }, + } = this.props; + + const { getChildren } = this.context; + return this.setState({ + items: await getChildren(itemId), + }); + }; + + renderItems = () => { + const { items } = this.state; + + if (!items.length) { + return ( + + No Item Here + + ); + } + + return items.reverse().map((item) => ( + + + + )); + }; + render() { - const { items } = this.context; return (
- {items.reverse().map((item) => ( - - - - ))} + {this.renderItems()}
); } } -export default Items; +export default withRouter(Items); diff --git a/src/components/main/ItemsHeader.js b/src/components/main/ItemsHeader.js index a05ae99cd..22479f548 100644 --- a/src/components/main/ItemsHeader.js +++ b/src/components/main/ItemsHeader.js @@ -1,8 +1,8 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; import Tooltip from '@material-ui/core/Tooltip'; import Info from '@material-ui/icons/Info'; +import Navigation from '../layout/Navigation'; const useStyles = makeStyles((theme) => ({ root: { @@ -17,7 +17,7 @@ const ItemsHeader = () => { const classes = useStyles(); return (
- Items + diff --git a/src/components/main/MoveItemModal.js b/src/components/main/MoveItemModal.js new file mode 100644 index 000000000..681851515 --- /dev/null +++ b/src/components/main/MoveItemModal.js @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import TreeView from '@material-ui/lab/TreeView'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import TreeItem from '@material-ui/lab/TreeItem'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Dialog from '@material-ui/core/Dialog'; +import { Button } from '@material-ui/core'; +import { ItemContext } from '../context/item'; +import { getItemTree } from '../../api/item'; + +const styles = () => ({ + root: { + height: 240, + flexGrow: 1, + maxWidth: 400, + }, +}); + +class MoveItemModal extends Component { + static propTypes = { + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + classes: PropTypes.shape({ + root: PropTypes.string.isRequired, + }).isRequired, + }; + + static contextType = ItemContext; + + state = { + items: [], + }; + + async componentDidMount() { + const { items } = this.context; + const tree = await getItemTree(items); + this.setState({ items: tree }); + } + + handleClose = () => { + const { onClose } = this.props; + onClose(); + }; + + onConfirm = () => { + const { onClose } = this.props; + // eslint-disable-next-line no-console + console.log('I choosed'); + onClose(); + }; + + onSelect = (e, value) => { + // eslint-disable-next-line no-console + console.log(value); + }; + + renderItemTreeItem = (items) => { + return items?.map(({ id, name, children }) => ( + + {this.renderItemTreeItem(children)} + + )); + }; + + render() { + const { items } = this.state; + const { open, classes } = this.props; + return ( + + + Where do you want to move the item? + + } + defaultExpandIcon={} + onNodeSelect={this.onSelect} + > + {this.renderItemTreeItem(items)} + + + + ); + } +} + +export default withStyles(styles)(MoveItemModal); diff --git a/src/config/paths.js b/src/config/paths.js index 6ebf8ea1d..c76327767 100644 --- a/src/config/paths.js +++ b/src/config/paths.js @@ -1,2 +1,4 @@ +export const HOME_PATH = '/'; export const SIGN_IN_PATH = '/signIn'; export const SIGN_UP_PATH = '/signUp'; +export const ITEMS_PATH = '/items';