diff --git a/cypress/fixtures/items.js b/cypress/fixtures/items.js index a21c34358..1fd2cbc3a 100644 --- a/cypress/fixtures/items.js +++ b/cypress/fixtures/items.js @@ -22,12 +22,18 @@ export const SIMPLE_ITEMS = [ id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', name: 'own_item_name1', path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, }, { ...DEFAULT_ITEM, id: 'fdf09f5a-5688-11eb-ae93-0242ac130002', name: 'own_item_name2', path: 'fdf09f5a_5688_11eb_ae93_0242ac130002', + extra: { + image: 'someimageurl', + }, }, { ...DEFAULT_ITEM, @@ -35,6 +41,9 @@ export const SIMPLE_ITEMS = [ name: 'own_item_name3', path: 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130003', + extra: { + image: 'someimageurl', + }, }, { ...DEFAULT_ITEM, @@ -42,5 +51,8 @@ export const SIMPLE_ITEMS = [ name: 'own_item_name4', path: 'ecafbd2a_5688_11eb_ae93_0242ac130002.fdf09f5a_5688_11eb_ae93_0242ac130004', + extra: { + image: 'someimageurl', + }, }, ]; diff --git a/cypress/integration/createItem.spec.js b/cypress/integration/createItem.spec.js index 134874f2e..0ce55d136 100644 --- a/cypress/integration/createItem.spec.js +++ b/cypress/integration/createItem.spec.js @@ -2,11 +2,11 @@ import { buildItemPath } from '../../src/config/paths'; import { buildItemCard, CREATE_ITEM_BUTTON_ID, - NEW_ITEM_CONFIRM_BUTTON_ID, - NEW_ITEM_DESCRIPTION_INPUT_ID, - NEW_ITEM_IMAGE_INPUT_ID, - NEW_ITEM_NAME_INPUT_ID, - NEW_ITEM_TYPE_SELECT_ID, + ITEM_FORM_CONFIRM_BUTTON_ID, + ITEM_FORM_DESCRIPTION_INPUT_ID, + ITEM_FORM_IMAGE_INPUT_ID, + ITEM_FORM_NAME_INPUT_ID, + ITEM_FORM_TYPE_SELECT_ID, } from '../../src/config/selectors'; import { CREATED_ITEM, SIMPLE_ITEMS } from '../fixtures/items'; @@ -18,15 +18,15 @@ const createItem = ({ }) => { cy.get(`#${CREATE_ITEM_BUTTON_ID}`).click(); - cy.get(`#${NEW_ITEM_NAME_INPUT_ID}`).type(name); + cy.get(`#${ITEM_FORM_NAME_INPUT_ID}`).type(name); - cy.get(`#${NEW_ITEM_DESCRIPTION_INPUT_ID}`).type(description); + cy.get(`#${ITEM_FORM_DESCRIPTION_INPUT_ID}`).type(description); - cy.get(`#${NEW_ITEM_TYPE_SELECT_ID}`).click(); + cy.get(`#${ITEM_FORM_TYPE_SELECT_ID}`).click(); cy.get(`li[data-value="${type}"]`).click(); - cy.get(`#${NEW_ITEM_IMAGE_INPUT_ID}`).type(extra.image); + cy.get(`#${ITEM_FORM_IMAGE_INPUT_ID}`).type(extra.image); - cy.get(`#${NEW_ITEM_CONFIRM_BUTTON_ID}`).click(); + cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).click(); }; describe('Create Item', () => { diff --git a/cypress/integration/editItem.spec.js b/cypress/integration/editItem.spec.js new file mode 100644 index 000000000..b9e00eb9f --- /dev/null +++ b/cypress/integration/editItem.spec.js @@ -0,0 +1,97 @@ +import { buildItemPath } from '../../src/config/paths'; +import { + buildItemCard, + buildItemLink, + buildItemMenu, + ITEM_MENU_BUTTON_CLASS, + ITEM_MENU_EDIT_BUTTON_CLASS, + ITEM_FORM_CONFIRM_BUTTON_ID, + ITEM_FORM_DESCRIPTION_INPUT_ID, + ITEM_FORM_IMAGE_INPUT_ID, + ITEM_FORM_NAME_INPUT_ID, + ITEM_FORM_TYPE_SELECT_ID, +} from '../../src/config/selectors'; +import { SIMPLE_ITEMS } from '../fixtures/items'; + +const editItem = ({ + id, + name = '', + type = 'Space', + extra = {}, + description = '', +}) => { + const menuSelector = `#${buildItemCard(id)} .${ITEM_MENU_BUTTON_CLASS}`; + cy.get(menuSelector).click(); + cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_EDIT_BUTTON_CLASS}`).click(); + + cy.get(`#${ITEM_FORM_NAME_INPUT_ID}`).clear().type(name); + + cy.get(`#${ITEM_FORM_DESCRIPTION_INPUT_ID}`).clear().type(description); + + cy.get(`#${ITEM_FORM_TYPE_SELECT_ID}`).click(); + cy.get(`li[data-value="${type}"]`).click(); + cy.get(`#${ITEM_FORM_IMAGE_INPUT_ID}`).clear().type(extra.image); + + cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).click(); +}; + +describe('Edit Item', () => { + it('edit item on Home', () => { + cy.setUpApi({ items: SIMPLE_ITEMS }); + cy.visit('/'); + + const itemToEdit = SIMPLE_ITEMS[0]; + const newName = 'new name'; + const newDescription = 'new description'; + + // create + editItem({ + ...itemToEdit, + name: newName, + description: newDescription, + }); + + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, + }, + }) => { + // check item is created and displayed + cy.wait(1000); + cy.get(`#${buildItemCard(id)}`).should('exist'); + cy.get(`#${buildItemLink(id)}`).contains(name); + }, + ); + }); + + it('create item in item', () => { + cy.setUpApi({ items: SIMPLE_ITEMS }); + // go to children item + cy.visit(buildItemPath(SIMPLE_ITEMS[0].id)); + + const itemToEdit = SIMPLE_ITEMS[2]; + const newName = 'new name'; + const newDescription = 'new description'; + + // create + editItem({ + ...itemToEdit, + name: newName, + description: newDescription, + }); + + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, + }, + }) => { + // check item is created and displayed + cy.wait(1000); + cy.get(`#${buildItemCard(id)}`).should('exist'); + cy.get(`#${buildItemLink(id)}`).contains(name); + }, + ); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 479c370a4..d84a4545d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -11,6 +11,7 @@ import { mockGetOwnItems, mockMoveItem, mockPostItem, + mockEditItem, } from './server'; Cypress.Commands.add( @@ -22,6 +23,7 @@ Cypress.Commands.add( moveItemError = false, copyItemError = false, getItemError = false, + editItemError = false, } = {}) => { const cachedItems = JSON.parse(JSON.stringify(items)); @@ -38,6 +40,8 @@ Cypress.Commands.add( mockMoveItem(cachedItems, moveItemError); mockCopyItem(cachedItems, copyItemError); + + mockEditItem(cachedItems, editItemError); }, ); diff --git a/cypress/support/server.js b/cypress/support/server.js index df20408aa..ddeb57f46 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import { buildCopyItemRoute, buildDeleteItemRoute, + buildEditItemRoute, buildGetChildrenRoute, buildGetItemRoute, buildMoveItemRoute, @@ -180,3 +181,19 @@ export const mockCopyItem = (items, shouldThrowError) => { }, ).as('copyItem'); }; + +export const mockEditItem = (items, shouldThrowError) => { + cy.intercept( + { + method: 'PATCH', + url: new RegExp(`${API_HOST}/${buildEditItemRoute(ID_FORMAT)}`), + }, + ({ reply, body }) => { + if (shouldThrowError) { + return reply({ statusCode: ERROR_CODE }); + } + + return reply(body); + }, + ).as('editItem'); +}; diff --git a/src/actions/item.js b/src/actions/item.js index e3b6c6bd2..bf86c744d 100644 --- a/src/actions/item.js +++ b/src/actions/item.js @@ -18,7 +18,9 @@ import { FLAG_GETTING_ITEMS, FLAG_MOVING_ITEM, FLAG_COPYING_ITEM, + EDIT_ITEM_SUCCESS, FLAG_SETTING_ITEM, + FLAG_EDITING_ITEM, } from '../types/item'; import { getParentsIdsFromPath } from '../utils/item'; import { createFlag } from './utils'; @@ -209,3 +211,18 @@ export const getChildren = (id) => async (dispatch) => { dispatch(createFlag(FLAG_GETTING_CHILDREN, false)); } }; + +export const editItem = (item) => async (dispatch) => { + try { + dispatch(createFlag(FLAG_EDITING_ITEM, true)); + const editedItem = await Api.editItem(item); + dispatch({ + type: EDIT_ITEM_SUCCESS, + payload: editedItem, + }); + } catch (e) { + console.error(e); + } finally { + dispatch(createFlag(FLAG_EDITING_ITEM, false)); + } +}; diff --git a/src/actions/layout.js b/src/actions/layout.js index f8c0c5fcb..bd810deb3 100644 --- a/src/actions/layout.js +++ b/src/actions/layout.js @@ -1,5 +1,6 @@ import { SET_COPY_MODAL_SETTINGS, + SET_EDIT_MODAL_SETTINGS, SET_MOVE_MODAL_SETTINGS, } from '../types/layout'; @@ -16,3 +17,10 @@ export const setCopyModalSettings = (payload) => (dispatch) => { payload, }); }; + +export const setEditModalSettings = (payload) => (dispatch) => { + dispatch({ + type: SET_EDIT_MODAL_SETTINGS, + payload, + }); +}; diff --git a/src/api/item.js b/src/api/item.js index 05c467939..c34586165 100644 --- a/src/api/item.js +++ b/src/api/item.js @@ -2,13 +2,19 @@ import { API_HOST, ROOT_ID } from '../config/constants'; import { buildCopyItemRoute, buildDeleteItemRoute, + buildEditItemRoute, buildGetChildrenRoute, buildGetItemRoute, buildMoveItemRoute, buildPostItemRoute, GET_OWN_ITEMS_ROUTE, } from './routes'; -import { DEFAULT_DELETE, DEFAULT_GET, DEFAULT_POST } from './utils'; +import { + DEFAULT_DELETE, + DEFAULT_GET, + DEFAULT_POST, + DEFAULT_PATCH, +} from './utils'; import * as CacheOperations from '../config/cache'; export const getItem = async (id) => { @@ -80,6 +86,26 @@ export const deleteItem = async (id) => { return res.json(); }; +// payload = {name, type, description, extra} +// querystring = {parentId} +export const editItem = async (item) => { + const req = await fetch(`${API_HOST}/${buildEditItemRoute(item.id)}`, { + ...DEFAULT_PATCH, + body: JSON.stringify(item), + }); + + if (!req.ok) { + throw new Error((await req.json()).message); + } + + const newItem = await req.json(); + + await CacheOperations.saveItem(newItem); + + return newItem; +}; + +// we need this function for navigation purposes: when you click on an item, you want to see its 'immediate' children export const getChildren = async (id) => { const res = await fetch( `${API_HOST}/${buildGetChildrenRoute(id)}`, diff --git a/src/api/routes.js b/src/api/routes.js index e9ed645f4..a282319d1 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -11,3 +11,4 @@ export const buildGetChildrenRoute = (id) => `items/${id}/children`; export const buildGetItemRoute = (id) => `items/${id}`; export const buildMoveItemRoute = (id) => `items/${id}/move`; export const buildCopyItemRoute = (id) => `items/${id}/copy`; +export const buildEditItemRoute = (id) => `items/${id}`; diff --git a/src/api/utils.js b/src/api/utils.js index 3c1144be3..048724db9 100644 --- a/src/api/utils.js +++ b/src/api/utils.js @@ -14,3 +14,9 @@ export const DEFAULT_DELETE = { method: 'DELETE', credentials: 'include', }; + +export const DEFAULT_PATCH = { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', +}; diff --git a/src/components/main/CustomCardHeader.js b/src/components/main/CustomCardHeader.js index 2e151aaca..c2210aa2e 100644 --- a/src/components/main/CustomCardHeader.js +++ b/src/components/main/CustomCardHeader.js @@ -37,7 +37,8 @@ const useStyles = makeStyles((theme) => ({ }, })); -const CustomCardHeader = ({ id, creator, title, type }) => { +const CustomCardHeader = ({ item }) => { + const { id, creator, name, type } = item; const classes = useStyles(); const { t } = useTranslation(); return ( @@ -47,7 +48,7 @@ const CustomCardHeader = ({ id, creator, title, type }) => {
- {title} + {name} @@ -58,16 +59,18 @@ const CustomCardHeader = ({ id, creator, title, type }) => {
- + ); }; CustomCardHeader.propTypes = { - id: PropTypes.string.isRequired, - creator: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, + item: PropTypes.shape({ + id: PropTypes.string.isRequired, + creator: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, }; export default CustomCardHeader; diff --git a/src/components/main/EditItemModal.js b/src/components/main/EditItemModal.js new file mode 100644 index 000000000..c2a0e09d6 --- /dev/null +++ b/src/components/main/EditItemModal.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { Map, List } from 'immutable'; +import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { editItem } from '../../actions/item'; +import ItemForm from './ItemForm'; +import { setEditModalSettings } from '../../actions/layout'; +import { getItemById } from '../../utils/item'; + +const EditItemModal = ({ + dispatchEditItem, + dispatchSetEditModalSettings, + settings, + items, +}) => { + const { t } = useTranslation(); + + const submitChanges = async (data) => { + dispatchEditItem({ + ...data, + }); + }; + + const handleClose = () => { + dispatchSetEditModalSettings({ open: false, itemId: null }); + }; + + const selectedId = settings.get('itemId'); + const item = selectedId ? getItemById(items, selectedId) : null; + + return ( + + ); +}; + +EditItemModal.propTypes = { + dispatchSetEditModalSettings: PropTypes.func.isRequired, + dispatchEditItem: PropTypes.func.isRequired, + settings: PropTypes.instanceOf(Map).isRequired, + items: PropTypes.instanceOf(List).isRequired, +}; + +const mapStateToProps = ({ item, layout }) => ({ + parentId: item.getIn(['item', 'id']), + settings: layout.get('editModal'), + items: item.get('items'), +}); + +const mapDispatchToProps = { + dispatchEditItem: editItem, + dispatchSetEditModalSettings: setEditModalSettings, +}; + +const ConnectedComponent = connect( + mapStateToProps, + mapDispatchToProps, +)(EditItemModal); + +export default ConnectedComponent; diff --git a/src/components/main/Item.js b/src/components/main/Item.js index 0f8b8d3e4..1e6d05767 100644 --- a/src/components/main/Item.js +++ b/src/components/main/Item.js @@ -33,11 +33,11 @@ const useStyles = makeStyles(() => ({ const Item = ({ item, dispatchDeleteItem }) => { const classes = useStyles(); - const { id, name, description, creator, type, extra } = item; + const { id, name, description, extra } = item; return ( - + ({ + dialogContent: { + display: 'flex', + flexDirection: 'column', + }, + shortInputField: { + width: '50%', + }, + addedMargin: { + marginTop: theme.spacing(2), + }, +}); + +class ItemForm extends Component { + static propTypes = { + onConfirm: PropTypes.func.isRequired, + classes: PropTypes.shape({ + shortInputField: PropTypes.string.isRequired, + dialogContent: PropTypes.string.isRequired, + addedMargin: PropTypes.string.isRequired, + }).isRequired, + handleClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + id: PropTypes.string, + title: PropTypes.string, + item: PropTypes.shape({ + name: PropTypes.string, + description: PropTypes.string, + type: PropTypes.string, + extra: PropTypes.shape({ + image: PropTypes.string, + }), + }), + confirmText: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, + }; + + static defaultProps = { + id: '', + title: null, + item: {}, + }; + + state = { + itemName: '', + itemType: ITEM_TYPES.SPACE, + itemDescription: '', + itemImageUrl: '', + }; + + componentDidMount() { + this.setDefaultValues(); + } + + componentDidUpdate({ item: prevItem }) { + const { item } = this.props; + if (!areItemsEqual(item, prevItem)) { + this.setDefaultValues(); + } + } + + setDefaultValues = () => { + const { item } = this.props; + const { + name: itemName = '', + type: itemType = ITEM_TYPES.SPACE, + description: itemDescription = '', + extra = {}, + } = item || {}; + const { image: itemImageUrl = '' } = extra; + this.setState({ + itemName, + itemType, + itemDescription, + itemImageUrl, + }); + }; + + handleNameInput = (event) => { + this.setState({ itemName: event.target.value }); + }; + + handleItemSelect = (event) => { + this.setState({ itemType: event.target.value }); + }; + + handleDescriptionInput = (event) => { + this.setState({ itemDescription: event.target.value }); + }; + + handleImageUrlInput = (event) => { + this.setState({ itemImageUrl: event.target.value }); + }; + + onClose = () => { + const { handleClose } = this.props; + handleClose(); + }; + + submit = () => { + const { onConfirm, item } = this.props; + const { itemName, itemType, itemDescription, itemImageUrl } = this.state; + onConfirm({ + ...item, + name: itemName, + type: itemType, + description: itemDescription, + extra: { image: itemImageUrl }, + }); + this.onClose(); + }; + + render() { + const { open, title, classes, t, confirmText, id } = this.props; + const { itemName, itemType, itemDescription, itemImageUrl } = this.state; + return ( + + {title || t('Item Form')} + + + + {t('Type')} + + + + + + + + + + + ); + } +} + +const TranslatedComponent = withTranslation()(ItemForm); +export default withStyles(styles)(TranslatedComponent); diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js index 24e294fcb..461bf9e2b 100644 --- a/src/components/main/ItemMenu.js +++ b/src/components/main/ItemMenu.js @@ -9,18 +9,22 @@ import MoreVertIcon from '@material-ui/icons/MoreVert'; import { setMoveModalSettings, setCopyModalSettings, + setEditModalSettings, } from '../../actions/layout'; import { buildItemMenu, ITEM_MENU_BUTTON_CLASS, ITEM_MENU_COPY_BUTTON_CLASS, + ITEM_MENU_EDIT_BUTTON_CLASS, ITEM_MENU_MOVE_BUTTON_CLASS, } from '../../config/selectors'; +import { editItem } from '../../actions/item'; const ItemMenu = ({ - itemId, + item, dispatchSetMoveModalSettings, dispatchSetCopyModalSettings, + dispatchSetEditModalSettings, }) => { const [anchorEl, setAnchorEl] = React.useState(null); const { t } = useTranslation(); @@ -34,12 +38,17 @@ const ItemMenu = ({ }; const handleMove = () => { - dispatchSetMoveModalSettings({ open: true, itemId }); + dispatchSetMoveModalSettings({ open: true, itemId: item.id }); handleClose(); }; const handleCopy = () => { - dispatchSetCopyModalSettings({ open: true, itemId }); + dispatchSetCopyModalSettings({ open: true, itemId: item.id }); + handleClose(); + }; + + const handleEdit = () => { + dispatchSetEditModalSettings({ open: true, itemId: item.id }); handleClose(); }; @@ -49,12 +58,15 @@ const ItemMenu = ({ + + {t('Edit')} + {t('Move')} @@ -67,7 +79,10 @@ const ItemMenu = ({ }; ItemMenu.propTypes = { - itemId: PropTypes.string.isRequired, + dispatchSetEditModalSettings: PropTypes.func.isRequired, + item: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, dispatchSetMoveModalSettings: PropTypes.func.isRequired, dispatchSetCopyModalSettings: PropTypes.func.isRequired, }; @@ -75,6 +90,8 @@ ItemMenu.propTypes = { const mapDispatchToProps = { dispatchSetMoveModalSettings: setMoveModalSettings, dispatchSetCopyModalSettings: setCopyModalSettings, + dispatchSetEditModalSettings: setEditModalSettings, + dispatchEditItem: editItem, }; const ConnectedComponent = connect(null, mapDispatchToProps)(ItemMenu); diff --git a/src/components/main/ItemsGrid.js b/src/components/main/ItemsGrid.js index f044016a8..eebb25c55 100644 --- a/src/components/main/ItemsGrid.js +++ b/src/components/main/ItemsGrid.js @@ -9,6 +9,7 @@ import Item from './Item'; import MoveItemModal from './MoveItemModal'; import CopyItemModal from './CopyItemModal'; import { ITEMS_GRID_NO_ITEM_ID } from '../../config/selectors'; +import EditItemModal from './EditItemModal'; class ItemsGrid extends Component { static propTypes = { @@ -51,6 +52,7 @@ class ItemsGrid extends Component { + ); } diff --git a/src/components/main/NewItemModal.js b/src/components/main/NewItemModal.js index 1ca7bceb4..bbdf8e438 100644 --- a/src/components/main/NewItemModal.js +++ b/src/components/main/NewItemModal.js @@ -1,160 +1,39 @@ -import React, { useState } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import TextField from '@material-ui/core/TextField'; -import { useTranslation } from 'react-i18next'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import InputLabel from '@material-ui/core/InputLabel'; -import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; import { createItem } from '../../actions/item'; -import { - NEW_ITEM_CONFIRM_BUTTON_ID, - NEW_ITEM_DESCRIPTION_INPUT_ID, - NEW_ITEM_IMAGE_INPUT_ID, - NEW_ITEM_NAME_INPUT_ID, - NEW_ITEM_TYPE_SELECT_ID, -} from '../../config/selectors'; - -const useStyles = makeStyles((theme) => ({ - dialogContent: { - display: 'flex', - flexDirection: 'column', - }, - shortInputField: { - width: '50%', - }, - addedMargin: { - marginTop: theme.spacing(2), - }, -})); +import ItemForm from './ItemForm'; -const CreateNewItem = ({ open, handleClose, dispatchCreateItem, parentId }) => { - const classes = useStyles(); - const [itemName, setItemName] = useState(''); - const [itemType, setItemType] = useState('Space'); - const [itemDescription, setItemDescription] = useState(''); - const [itemImageUrl, setItemImageUrl] = useState(''); +const NewItemModal = ({ open, handleClose, dispatchCreateItem, parentId }) => { const { t } = useTranslation(); - const handleNameInput = (event) => { - setItemName(event.target.value); - }; - - const handleItemSelect = (event) => { - setItemType(event.target.value); - }; - - const handleDescriptionInput = (event) => { - setItemDescription(event.target.value); - }; - - const handleImageUrlInput = (event) => { - setItemImageUrl(event.target.value); - }; - - const submitNewItem = async () => { + const submitNewItem = async (data) => { dispatchCreateItem({ parentId, - name: itemName, - type: itemType, - description: itemDescription, - extra: { image: itemImageUrl }, + ...data, }); }; return ( - { - setItemType(''); - handleClose(); - }} - maxWidth="sm" - fullWidth - > - {t('Create new item')} - - - - {t('Type')} - - - - - - - - - - + handleClose={handleClose} + title={t('Add new Item')} + confirmText={t('Add Item')} + /> ); }; -CreateNewItem.propTypes = { +NewItemModal.propTypes = { open: PropTypes.bool, handleClose: PropTypes.func.isRequired, dispatchCreateItem: PropTypes.func.isRequired, parentId: PropTypes.string, }; -CreateNewItem.defaultProps = { +NewItemModal.defaultProps = { open: false, parentId: null, }; @@ -170,6 +49,6 @@ const mapDispatchToProps = { const ConnectedComponent = connect( mapStateToProps, mapDispatchToProps, -)(CreateNewItem); +)(NewItemModal); export default ConnectedComponent; diff --git a/src/components/main/TreeModal.js b/src/components/main/TreeModal.js index 334cfedc0..4a01c3832 100644 --- a/src/components/main/TreeModal.js +++ b/src/components/main/TreeModal.js @@ -77,29 +77,20 @@ class TreeModal extends Component { componentDidMount() { const { dispatchGetOwnItems } = this.props; dispatchGetOwnItems(); - } - shouldComponentUpdate({ settings }) { - // update only when opened or on close - const { settings: prevSettings } = this.props; - const prevItemId = prevSettings.get('itemId'); - const open = settings.get('open'); - return open || (!open && Boolean(prevItemId)); + this.updateExpandedElements(); } componentDidUpdate({ settings: prevSettings, items: prevItems }) { const { dispatchGetItems, settings, items } = this.props; - const { expandedItems } = this.state; - + const prevOpen = prevSettings.get('open'); // expand tree until current item const itemId = settings.get('itemId'); + const open = settings.get('open'); const item = getItemById(items, itemId); const prevItem = prevItems.find(({ id }) => id === itemId); - if (!areItemsEqual(item, prevItem)) { - const parentIds = getParentsIdsFromPath(item.path) || []; - const newExpandedItems = [...expandedItems, ...parentIds]; - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ expandedItems: newExpandedItems }); + if (!areItemsEqual(item, prevItem) || (!prevOpen && open)) { + this.updateExpandedElements(); } if (settings.get('open') && !prevSettings.get('open')) { @@ -107,6 +98,23 @@ class TreeModal extends Component { } } + updateExpandedElements = () => { + const { settings, items } = this.props; + const { expandedItems } = this.state; + const itemId = settings.get('itemId'); + const item = getItemById(items, itemId); + if (item) { + const parentIds = getParentsIdsFromPath(item.path) || []; + // eslint-disable-next-line no-console + console.log('parentIds: ', parentIds); + const newExpandedItems = [...expandedItems, ...parentIds]; + // eslint-disable-next-line no-console + console.log('newExpandedItems: ', newExpandedItems); + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ expandedItems: newExpandedItems }); + } + }; + handleClose = () => { const { onClose } = this.props; onClose({ id: null, open: false }); diff --git a/src/config/constants.js b/src/config/constants.js index 50de9b544..ac0b51dfa 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -17,3 +17,9 @@ export const TREE_PREVENT_SELECTION = { export const TREE_VIEW_HEIGHT = 200; export const TREE_VIEW_MAX_WIDTH = 400; export const UUID_LENGTH = 36; + +export const ITEM_TYPES = { + SPACE: 'Space', + APPLICATION: 'Application', + EXERCISE: 'Exercise', +}; diff --git a/src/config/selectors.js b/src/config/selectors.js index fe5908c9d..37f0e3f29 100644 --- a/src/config/selectors.js +++ b/src/config/selectors.js @@ -1,11 +1,11 @@ export const ITEM_DELETE_BUTTON_CLASS = 'itemDeleteButton'; export const buildItemCard = (id) => `itemCard-${id}`; export const CREATE_ITEM_BUTTON_ID = 'createItemButton'; -export const NEW_ITEM_NAME_INPUT_ID = 'newItemNameInput'; -export const NEW_ITEM_TYPE_SELECT_ID = 'newItemTypeSelect'; -export const NEW_ITEM_DESCRIPTION_INPUT_ID = 'newItemDescriptionInput'; -export const NEW_ITEM_IMAGE_INPUT_ID = 'newItemImageInput'; -export const NEW_ITEM_CONFIRM_BUTTON_ID = 'newItemConfirmButton'; +export const ITEM_FORM_NAME_INPUT_ID = 'newItemNameInput'; +export const ITEM_FORM_TYPE_SELECT_ID = 'newItemTypeSelect'; +export const ITEM_FORM_DESCRIPTION_INPUT_ID = 'newItemDescriptionInput'; +export const ITEM_FORM_IMAGE_INPUT_ID = 'newItemImageInput'; +export const ITEM_FORM_CONFIRM_BUTTON_ID = 'newItemConfirmButton'; export const ITEM_SCREEN_ERROR_ALERT_ID = 'itemScreenErrorAlert'; export const buildItemLink = (id) => `itemLink-${id}`; export const NAVIGATION_HOME_LINK_ID = 'navigationHomeLink'; @@ -18,3 +18,4 @@ export const TREE_MODAL_TREE_ID = 'treeModalTree'; export const buildTreeItemClass = (id) => `treeItem-${id}`; export const TREE_MODAL_CONFIRM_BUTTON_ID = 'treeModalConfirmButton'; export const ITEMS_GRID_NO_ITEM_ID = 'itemsGridNoItem'; +export const ITEM_MENU_EDIT_BUTTON_CLASS = 'itemMenuEditButton'; diff --git a/src/data/sample.js b/src/data/sample.js index 7331276c4..2bb0b86a2 100644 --- a/src/data/sample.js +++ b/src/data/sample.js @@ -13,7 +13,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1581089778245-3ce67677f718?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68c8609dcab47158fda2', + path: '5f6c68c8609dcab47158fda2', }, { id: '5f6c68cee60b191d841f0fa6', @@ -29,7 +29,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1189&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68cee60b191d841f0fa6', + path: '5f6c68cee60b191d841f0fa6', }, { id: '5f6c68d3cc839782f16b68b8', @@ -45,7 +45,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68d3cc839782f16b68b8', + path: '5f6c68d3cc839782f16b68b8', }, { id: '5f6c68dacb58aae02ee8d56e', @@ -61,7 +61,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1528642474498-1af0c17fd8c3?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68dacb58aae02ee8d56e', + path: '5f6c68dacb58aae02ee8d56e', }, { id: '5f6c68e41786318beb0a79ff', @@ -77,7 +77,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1546289917-e018604f4afa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68e41786318beb0a79ff', + path: '5f6c68e41786318beb0a79ff', }, { id: '5f6c68ed5b0ba6ea3a1d26f0', @@ -93,7 +93,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1521587760476-6c12a4b040da?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80', }, - path: '5f6c689c2814719d2abcccfb-5f6c68ed5b0ba6ea3a1d26f0', + path: '5f6c68ed5b0ba6ea3a1d26f0', }, { id: '5f6c693e2aa702b99d5eb1fb', @@ -109,8 +109,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1189&q=80', }, - path: - '5f6c689c2814719d2abcccfb-5f6c68cee60b191d841f0fa6-5f6c693e2aa702b99d5eb1fb', + path: '5f6c68cee60b191d841f0fa6.5f6c693e2aa702b99d5eb1fb', }, { id: '5f6c696b49569c02af5e333c', @@ -126,8 +125,7 @@ const items = [ image: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1189&q=80', }, - path: - '5f6c689c2814719d2abcccfb-5f6c68cee60b191d841f0fa6-5f6c696b49569c02af5e333c', + path: '5f6c68cee60b191d841f0fa6.5f6c696b49569c02af5e333c', }, ]; diff --git a/src/langs/en.json b/src/langs/en.json index 7e442d651..a3eae3cce 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -31,6 +31,8 @@ "Add item": "Add item", "Copy": "Copy", "Where do you want to copy this item?": "Where do you want to copy this item?", - "Home":"Home" + "Home":"Home", + "Edit Item": "Edit Item", + "Edit": "Edit" } } diff --git a/src/langs/fr.json b/src/langs/fr.json index a4c2ffa28..b4ddf6ddf 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -28,6 +28,11 @@ "Owned Items": "Mes Éléments", "Type by author": "{{type}} par {{author}}", "Unknown": "Inconnu", - "Home":"Home" + "Home":"Home", + "Add item": "Ajouter un élément", + "Copy": "Copier", + "Where do you want to copy this item?": "Où copier cet élément?", + "Edit Item": "Modifier l'élément", + "Edit": "Modifier" } } diff --git a/src/reducers/item.js b/src/reducers/item.js index f889b8b66..bf1ba1b3b 100644 --- a/src/reducers/item.js +++ b/src/reducers/item.js @@ -1,6 +1,5 @@ import { Map, List } from 'immutable'; import { ROOT_ID } from '../config/constants'; -// import { ROOT_ID } from '../config/constants'; import { SET_ITEM_SUCCESS, DELETE_ITEM_SUCCESS, @@ -21,6 +20,8 @@ import { FLAG_MOVING_ITEM, FLAG_COPYING_ITEM, FLAG_SETTING_ITEM, + EDIT_ITEM_SUCCESS, + FLAG_EDITING_ITEM, } from '../types/item'; const DEFAULT_ITEM = Map({ @@ -42,6 +43,7 @@ const INITIAL_STATE = Map({ [FLAG_MOVING_ITEM]: [], [FLAG_COPYING_ITEM]: [], [FLAG_SETTING_ITEM]: [], + [FLAG_EDITING_ITEM]: [], }), }); @@ -52,7 +54,27 @@ const updateActivity = (payload) => (activity) => { return activity.slice(1); }; -const addInList = (item) => (list) => list.push(item); +const updateItemInList = (item, list) => { + const idx = list.findIndex(({ id }) => item.id === id); + if (idx < 0) { + return list.push(item); + } + return list.set(idx, item); +}; + +const updateInList = (els) => (list) => { + // add array of items + if (Array.isArray(els)) { + let newList = list; + els.forEach((item) => { + newList = updateItemInList(item, newList); + }); + return newList; + } + + // add one item + return updateItemInList(els, list); +}; const removeFromList = (deletedItemId) => (list) => list.filter(({ id }) => id !== deletedItemId); @@ -68,6 +90,7 @@ export default (state = INITIAL_STATE, { type, payload }) => { case FLAG_MOVING_ITEM: case FLAG_COPYING_ITEM: case FLAG_SETTING_ITEM: + case FLAG_EDITING_ITEM: return state.updateIn(['activity', type], updateActivity(payload)); case CLEAR_ITEM_SUCCESS: return state.setIn(['item'], DEFAULT_ITEM); @@ -77,18 +100,21 @@ export default (state = INITIAL_STATE, { type, payload }) => { case GET_ITEMS_SUCCESS: { return state.set('items', List(payload)); } - case SET_ITEM_SUCCESS: + case SET_ITEM_SUCCESS: { + const { item, parents, children } = payload; return state - .setIn(['item'], Map(payload.item)) - .setIn(['item', 'children'], List(payload.children)) - .setIn(['item', 'parents'], List(payload.parents)); + .setIn(['item'], Map(item)) + .setIn(['item', 'children'], List(children)) + .setIn(['item', 'parents'], List(parents)) + .updateIn(['items'], updateInList([...parents, ...children, item])); + } case CREATE_ITEM_SUCCESS: { const from = state.getIn(['item', 'id']); // add item in children or in root items if (!from) { - return state.update('rootItems', addInList(payload)); + return state.update('rootItems', updateInList(payload)); } - return state.updateIn(['item', 'children'], addInList(payload)); + return state.updateIn(['item', 'children'], updateInList(payload)); } case DELETE_ITEM_SUCCESS: case MOVE_ITEM_SUCCESS: { @@ -103,18 +129,29 @@ export default (state = INITIAL_STATE, { type, payload }) => { // add new item to current view const { to, item } = payload; if (to === state.getIn(['item', 'id'])) { - return state.updateIn(['item', 'children'], addInList(item)); + return state.updateIn(['item', 'children'], updateInList(item)); } if (to === ROOT_ID) { - return state.updateIn(['rootItems'], addInList(item)); + return state.updateIn(['rootItems'], updateInList(item)); } return state; } case GET_CHILDREN_SUCCESS: { - return state; + return state.updateIn(['items'], updateInList(payload.children)); } case GET_OWN_ITEMS_SUCCESS: { - return state.setIn(['rootItems'], List(payload)); + return state + .setIn(['rootItems'], List(payload)) + .updateIn(['items'], updateInList(payload)); + } + case EDIT_ITEM_SUCCESS: { + // update current elements + if (state.getIn(['item', 'id'])) { + return state.updateIn(['item', 'children'], updateInList(payload)); + } + + // update home elements + return state.updateIn(['rootItems'], updateInList(payload)); } default: return state; diff --git a/src/reducers/layout.js b/src/reducers/layout.js index 49b7a5055..14607a311 100644 --- a/src/reducers/layout.js +++ b/src/reducers/layout.js @@ -2,6 +2,7 @@ import { Map } from 'immutable'; import { SET_MOVE_MODAL_SETTINGS, SET_COPY_MODAL_SETTINGS, + SET_EDIT_MODAL_SETTINGS, } from '../types/layout'; const INITIAL_STATE = Map({ @@ -13,6 +14,10 @@ const INITIAL_STATE = Map({ open: false, itemId: null, }), + editModal: Map({ + open: false, + itemId: null, + }), }); export default (state = INITIAL_STATE, { type, payload }) => { @@ -21,6 +26,8 @@ export default (state = INITIAL_STATE, { type, payload }) => { return state.setIn(['copyModal'], Map(payload)); case SET_MOVE_MODAL_SETTINGS: return state.setIn(['moveModal'], Map(payload)); + case SET_EDIT_MODAL_SETTINGS: + return state.setIn(['editModal'], Map(payload)); default: return state; } diff --git a/src/types/item.js b/src/types/item.js index ef18dfa6c..3f4b68bb5 100644 --- a/src/types/item.js +++ b/src/types/item.js @@ -17,3 +17,5 @@ export const FLAG_GETTING_ITEMS = 'FLAG_GETTING_ITEMS'; export const FLAG_MOVING_ITEM = 'FLAG_MOVING_ITEM'; export const FLAG_COPYING_ITEM = 'FLAG_COPYING_ITEM'; export const FLAG_SETTING_ITEM = 'FLAG_SETTING_ITEM'; +export const EDIT_ITEM_SUCCESS = 'EDIT_ITEM_SUCCESS'; +export const FLAG_EDITING_ITEM = 'FLAG_EDITING_ITEM'; diff --git a/src/types/layout.js b/src/types/layout.js index 085c7828d..bbb2d3ec5 100644 --- a/src/types/layout.js +++ b/src/types/layout.js @@ -1,2 +1,3 @@ export const SET_MOVE_MODAL_SETTINGS = 'SET_MOVE_MODAL_SETTINGS'; export const SET_COPY_MODAL_SETTINGS = 'SET_COPY_MODAL_SETTINGS'; +export const SET_EDIT_MODAL_SETTINGS = 'SET_EDIT_MODAL_SETTINGS'; diff --git a/src/utils/common.js b/src/utils/common.js index 37882e181..ace090c52 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -11,7 +11,7 @@ export const isSignedIn = () => { // limit text length // fix: There must be a better way of doing it export const shortenString = (string, maxLength) => { - if (!string || string.length < maxLength) { + if (!string || string.length <= maxLength) { return string; } return `${string.split(' ').slice(0, maxLength).join(' ')}...`;