From 806ca5618aa089520cebd7baf4514d3c292474e7 Mon Sep 17 00:00:00 2001 From: pyphilia Date: Thu, 10 Dec 2020 15:34:43 +0100 Subject: [PATCH 1/3] feat: display shared items --- cypress/fixtures/items.js | 5 + cypress/integration/shareItem.spec.js | 62 ++++++++++ cypress/support/commands.js | 10 ++ cypress/support/server.js | 46 ++++++++ cypress/support/utils.js | 2 + src/actions/item.js | 40 +++++-- src/actions/layout.js | 8 ++ src/actions/member.js | 0 src/actions/membership.js | 10 ++ src/api/item.js | 13 +++ src/api/member.js | 26 +++++ src/api/membership.js | 35 ++++++ src/api/routes.js | 8 ++ src/components/App.js | 89 ++++++++------ src/components/main/Home.js | 41 +++++-- src/components/main/Item.js | 2 +- src/components/main/ItemMenu.js | 16 +++ src/components/main/ItemScreen.js | 17 ++- src/components/main/ItemsGrid.js | 7 -- src/components/main/ShareItemModal.js | 162 ++++++++++++++++++++++++++ src/components/main/TreeModal.js | 8 +- src/config/constants.js | 12 ++ src/config/errors.js | 2 + src/config/selectors.js | 6 + src/langs/en.json | 12 +- src/langs/fr.json | 5 +- src/reducers/item.js | 24 ++-- src/reducers/layout.js | 7 ++ src/types/item.js | 2 + src/types/layout.js | 1 + 30 files changed, 605 insertions(+), 73 deletions(-) create mode 100644 cypress/integration/shareItem.spec.js create mode 100644 src/actions/member.js create mode 100644 src/actions/membership.js create mode 100644 src/api/member.js create mode 100644 src/api/membership.js create mode 100644 src/components/main/ShareItemModal.js create mode 100644 src/config/errors.js diff --git a/cypress/fixtures/items.js b/cypress/fixtures/items.js index a603a5a81..64f2c786d 100644 --- a/cypress/fixtures/items.js +++ b/cypress/fixtures/items.js @@ -23,6 +23,11 @@ export const EDITED_FIELDS = { description: 'new description', }; +export const MEMBERS = { + ANNA: { id: 'anna-id', name: 'anna', email: 'anna@email.com' }, + BOB: { id: 'bob-id', name: 'bob', email: 'bob@email.com' }, +}; + export const SIMPLE_ITEMS = [ { ...DEFAULT_ITEM, diff --git a/cypress/integration/shareItem.spec.js b/cypress/integration/shareItem.spec.js new file mode 100644 index 000000000..1bac99970 --- /dev/null +++ b/cypress/integration/shareItem.spec.js @@ -0,0 +1,62 @@ +import { PERMISSION_LEVELS } from '../../src/config/constants'; +import { buildItemPath } from '../../src/config/paths'; +import { + buildItemCard, + buildItemMenu, + ITEM_MENU_BUTTON_CLASS, + ITEM_MENU_SHARE_BUTTON_CLASS, + SHARE_ITEM_MODAL_PERMISSION_SELECT_ID, + SHARE_ITEM_MODAL_SHARE_BUTTON_ID, + buildPermissionOptionId, + SHARE_ITEM_MODAL_EMAIL_INPUT_ID, +} from '../../src/config/selectors'; +import { MEMBERS, SIMPLE_ITEMS } from '../fixtures/items'; + +const shareItem = ({ id, member, permission }) => { + const menuSelector = `#${buildItemCard(id)} .${ITEM_MENU_BUTTON_CLASS}`; + cy.get(menuSelector).click(); + cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_SHARE_BUTTON_CLASS}`).click(); + + // select permission + cy.get(`#${SHARE_ITEM_MODAL_PERMISSION_SELECT_ID}`).click(); + cy.get(`#${buildPermissionOptionId(permission)}`).click(); + + // input mail + cy.get(`#${SHARE_ITEM_MODAL_EMAIL_INPUT_ID}`).type(member.email); + + cy.get(`#${SHARE_ITEM_MODAL_SHARE_BUTTON_ID}`).click(); +}; + +describe('Share Item', () => { + it('share item on Home', () => { + cy.setUpApi({ items: SIMPLE_ITEMS, members: Object.values(MEMBERS) }); + cy.visit('/'); + + // share + const { id } = SIMPLE_ITEMS[0]; + const member = MEMBERS.ANNA; + shareItem({ id, member, permission: PERMISSION_LEVELS.WRITE }); + + cy.wait('@shareItem').then(() => { + cy.get(`#${buildItemCard(id)}`).should('exist'); + }); + }); + + it('share item in item', () => { + cy.setUpApi({ items: SIMPLE_ITEMS, members: Object.values(MEMBERS) }); + + // go to children item + cy.visit(buildItemPath(SIMPLE_ITEMS[0].id)); + + // share + const { id } = SIMPLE_ITEMS[2]; + const member = MEMBERS.ANNA; + shareItem({ id, member, permission: PERMISSION_LEVELS.READ }); + + cy.wait('@shareItem').then(() => { + cy.get(`#${buildItemCard(id)}`).should('exist'); + }); + }); + + // todo : check item permission for users +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 49626442f..8f538ce4e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,20 +13,26 @@ import { mockMoveItem, mockPostItem, mockEditItem, + mockShareItem, + mockGetMember, } from './server'; Cypress.Commands.add( 'setUpApi', ({ items = [], + members = [], deleteItemError = false, postItemError = false, moveItemError = false, copyItemError = false, getItemError = false, editItemError = false, + shareItemError = false, + getMemberError = false, } = {}) => { const cachedItems = JSON.parse(JSON.stringify(items)); + const cachedMembers = JSON.parse(JSON.stringify(members)); mockGetOwnItems(cachedItems); @@ -43,6 +49,10 @@ Cypress.Commands.add( mockCopyItem(cachedItems, copyItemError); mockEditItem(cachedItems, editItemError); + + mockShareItem(cachedItems, shareItemError); + + mockGetMember(cachedMembers, getMemberError); }, ); diff --git a/cypress/support/server.js b/cypress/support/server.js index 15c812839..fb5de34ab 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -8,6 +8,8 @@ import { buildMoveItemRoute, buildPostItemRoute, GET_OWN_ITEMS_ROUTE, + buildShareItemWithRoute, + MEMBERS_ROUTE, } from '../../src/api/routes'; import { getItemById, @@ -21,6 +23,7 @@ import { ID_FORMAT, parseStringToRegExp, SUCCESS_CODE, + EMAIL_FORMAT, } from './utils'; import { DEFAULT_PATCH, @@ -203,3 +206,46 @@ export const mockEditItem = (items, shouldThrowError) => { }, ).as('editItem'); }; + +export const mockShareItem = (items, shouldThrowError) => { + cy.intercept( + { + method: DEFAULT_POST.method, + url: new RegExp( + `${API_HOST}/${parseStringToRegExp( + buildShareItemWithRoute(ID_FORMAT), + )}`, + ), + }, + ({ reply, body }) => { + if (shouldThrowError) { + return reply({ statusCode: ERROR_CODE }); + } + + return reply(body); + }, + ).as('shareItem'); +}; + +export const mockGetMember = (members, shouldThrowError) => { + const emailReg = new RegExp(EMAIL_FORMAT); + cy.intercept( + { + method: DEFAULT_GET.method, + pathname: MEMBERS_ROUTE, + query: { + email: emailReg, + }, + }, + ({ reply, url }) => { + if (shouldThrowError) { + return reply({ statusCode: ERROR_CODE }); + } + + const mail = emailReg.exec(url)[0]; + const member = members.find(({ email }) => email === mail); + + return reply([member]); + }, + ).as('getMember'); +}; diff --git a/cypress/support/utils.js b/cypress/support/utils.js index 05b5fcf8f..cf136b84f 100644 --- a/cypress/support/utils.js +++ b/cypress/support/utils.js @@ -5,3 +5,5 @@ export const ERROR_CODE = 400; export const SUCCESS_CODE = 200; export const parseStringToRegExp = (string) => string.replaceAll('?', '\\?'); + +export const EMAIL_FORMAT = '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+'; diff --git a/src/actions/item.js b/src/actions/item.js index bf86c744d..144c88681 100644 --- a/src/actions/item.js +++ b/src/actions/item.js @@ -21,6 +21,7 @@ import { EDIT_ITEM_SUCCESS, FLAG_SETTING_ITEM, FLAG_EDITING_ITEM, + GET_SHARED_ITEMS_SUCCESS, } from '../types/item'; import { getParentsIdsFromPath } from '../utils/item'; import { createFlag } from './utils'; @@ -40,19 +41,15 @@ export const setItem = (id) => async (dispatch) => { const item = await Api.getItem(id); const { children, parents } = item; - - // get children let newChildren = []; if (!children) { newChildren = await Api.getChildren(id); } - // get parents let newParents = []; if (!parents) { newParents = await buildParentsLine(item.path); } - dispatch({ type: SET_ITEM_SUCCESS, payload: { item, children: newChildren, parents: newParents }, @@ -132,14 +129,13 @@ export const createItem = (props) => async (dispatch) => { } }; -export const deleteItem = (id) => async (dispatch) => { +export const deleteItem = (item) => async (dispatch) => { try { dispatch(createFlag(FLAG_DELETING_ITEM, true)); - await Api.deleteItem(id); - + await Api.deleteItem(item.id); dispatch({ type: DELETE_ITEM_SUCCESS, - payload: id, + payload: item, }); } catch (e) { console.error(e); @@ -168,7 +164,7 @@ export const moveItem = (payload) => async (dispatch, getState) => { dispatch({ type: MOVE_ITEM_SUCCESS, - payload: payload.id, + payload, }); } catch (e) { console.error(e); @@ -226,3 +222,29 @@ export const editItem = (item) => async (dispatch) => { dispatch(createFlag(FLAG_EDITING_ITEM, false)); } }; + +export const getSharedItems = () => async (dispatch) => { + try { + const sharedItems = await Api.getSharedItems(); + + let childrenItems = []; + for (const item of sharedItems) { + const { children, id } = item; + // get children + let newChildren = []; + if (!children) { + // eslint-disable-next-line no-await-in-loop + newChildren = await Api.getChildren(id); + childrenItems = childrenItems.concat(newChildren); + item.children = newChildren.map(({ id: childId }) => childId); + } + } + + dispatch({ + type: GET_SHARED_ITEMS_SUCCESS, + payload: [...sharedItems, ...childrenItems], + }); + } catch (e) { + console.error(e); + } +}; diff --git a/src/actions/layout.js b/src/actions/layout.js index bd810deb3..df7007a12 100644 --- a/src/actions/layout.js +++ b/src/actions/layout.js @@ -2,6 +2,7 @@ import { SET_COPY_MODAL_SETTINGS, SET_EDIT_MODAL_SETTINGS, SET_MOVE_MODAL_SETTINGS, + SET_SHARE_MODAL_SETTINGS, } from '../types/layout'; export const setMoveModalSettings = (payload) => (dispatch) => { @@ -24,3 +25,10 @@ export const setEditModalSettings = (payload) => (dispatch) => { payload, }); }; + +export const setShareModalSettings = (payload) => (dispatch) => { + dispatch({ + type: SET_SHARE_MODAL_SETTINGS, + payload, + }); +}; diff --git a/src/actions/member.js b/src/actions/member.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/actions/membership.js b/src/actions/membership.js new file mode 100644 index 000000000..3cef2e75d --- /dev/null +++ b/src/actions/membership.js @@ -0,0 +1,10 @@ +import * as Api from '../api/membership'; + +// eslint-disable-next-line import/prefer-default-export +export const shareItemWith = async ({ id, email, permission }) => { + try { + await Api.shareItemWith({ id, email, permission }); + } catch (e) { + console.error(e); + } +}; diff --git a/src/api/item.js b/src/api/item.js index c34586165..8e2290ac1 100644 --- a/src/api/item.js +++ b/src/api/item.js @@ -8,6 +8,7 @@ import { buildMoveItemRoute, buildPostItemRoute, GET_OWN_ITEMS_ROUTE, + SHARE_ITEM_WITH_ROUTE, } from './routes'; import { DEFAULT_DELETE, @@ -161,3 +162,15 @@ export const copyItem = async ({ id, to }) => { return newItem; }; + +export const getSharedItems = async () => { + const req = await fetch(`${API_HOST}/${SHARE_ITEM_WITH_ROUTE}`, { + ...DEFAULT_GET, + }); + + if (req.status !== 200) { + throw new Error(req); + } + + return req.json(); +}; diff --git a/src/api/member.js b/src/api/member.js new file mode 100644 index 000000000..d15f74cda --- /dev/null +++ b/src/api/member.js @@ -0,0 +1,26 @@ +import fetch from 'node-fetch'; +import { API_HOST } from '../config/constants'; +import { DEFAULT_GET } from './utils'; +import { buildGetMemberBy, buildGetMember } from './routes'; + +// eslint-disable-next-line import/prefer-default-export +export const getMemberBy = async ({ email }) => { + const res = await fetch(`${API_HOST}/${buildGetMemberBy(email)}`, { + ...DEFAULT_GET, + }); + if (!res.ok) { + throw new Error((await res.json()).message); + } + + return res.json(); +}; + +export const getMember = async ({ id }) => { + const res = await fetch(`${API_HOST}/${buildGetMember(id)}`, { + ...DEFAULT_GET, + }); + if (!res.ok) { + throw new Error((await res.json()).message); + } + return res.json(); +}; diff --git a/src/api/membership.js b/src/api/membership.js new file mode 100644 index 000000000..870f70d77 --- /dev/null +++ b/src/api/membership.js @@ -0,0 +1,35 @@ +import { getMemberBy } from './member'; +import { API_HOST } from '../config/constants'; +import { DEFAULT_GET, DEFAULT_POST } from './utils'; +import { + buildShareItemWithRoute, + buildGetItemMembershipForItemRoute, +} from './routes'; +import { MEMBER_NOT_FOUND_ERROR } from '../config/errors'; + +export const getMembershipsForItem = async ({ id }) => { + const req = await fetch( + `${API_HOST}/${buildGetItemMembershipForItemRoute(id)}`, + DEFAULT_GET, + ); + + if (req.status !== 200) { + throw new Error(req); + } + + return req.json(); +}; + +export const shareItemWith = async ({ id, email, permission }) => { + const member = await getMemberBy({ email }); + // eslint-disable-next-line no-console + if (!member) { + throw new Error(MEMBER_NOT_FOUND_ERROR); + } + const req = await fetch(`${API_HOST}/${buildShareItemWithRoute(id)}`, { + ...DEFAULT_POST, + body: JSON.stringify({ memberId: member[0].id, permission }), // supposed to have only one member for this mail + }); + + return req.status === 200; +}; diff --git a/src/api/routes.js b/src/api/routes.js index a282319d1..1ac336e46 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -1,4 +1,5 @@ export const GET_OWN_ITEMS_ROUTE = `items/own`; +export const SHARE_ITEM_WITH_ROUTE = 'items/shared-with'; export const buildPostItemRoute = (parentId) => { let url = `items`; if (parentId) { @@ -12,3 +13,10 @@ 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}`; +export const buildShareItemWithRoute = (id) => `item-memberships?itemId=${id}`; +export const buildGetItemMembershipForItemRoute = (id) => + `item-memberships?itemId=${id}`; + +export const MEMBERS_ROUTE = `/members`; +export const buildGetMemberBy = (email) => `${MEMBERS_ROUTE}?email=${email}`; +export const buildGetMember = (id) => `${MEMBERS_ROUTE}/${id}`; diff --git a/src/components/App.js b/src/components/App.js index b386500e4..8c1bb4d1f 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,11 +1,12 @@ -import React from 'react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import { BrowserRouter as Router, Switch, Route, Redirect, } from 'react-router-dom'; -import { makeStyles } from '@material-ui/core/styles'; +import { withStyles } from '@material-ui/core'; import Header from './layout/Header'; import items from '../data/sample'; import SignUp from './SignUp'; @@ -18,42 +19,62 @@ import { import SignIn from './SignIn'; import Home from './main/Home'; import ItemScreen from './main/ItemScreen'; +import MoveItemModal from './main/MoveItemModal'; +import EditItemModal from './main/EditItemModal'; +import CopyItemModal from './main/CopyItemModal'; +import ShareItemModal from './main/ShareItemModal'; -const useStyles = makeStyles((theme) => ({ +const styles = (theme) => ({ root: { padding: theme.spacing(3), }, -})); +}); +// eslint-disable-next-line react/prefer-stateless-function +class App extends Component { + static propTypes = { + classes: PropTypes.shape({ + root: PropTypes.string.isRequired, + }).isRequired, + }; -function App() { - const classes = useStyles(); - return ( - -
-
-
- - - - - - - - - - - - - - - - - - -
-
-
- ); + render() { + const { classes } = this.props; + return ( + <> + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ + ); + } } -export default App; +const StyledComponent = withStyles(styles)(App); + +export default StyledComponent; diff --git a/src/components/main/Home.js b/src/components/main/Home.js index 3d693e700..709bb8008 100644 --- a/src/components/main/Home.js +++ b/src/components/main/Home.js @@ -1,10 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; import { withRouter } from 'react-router'; import ItemsHeader from './ItemsHeader'; import NewItemButton from './NewItemButton'; -import { getOwnItems } from '../../actions/item'; +import { setItem, getOwnItems, getSharedItems } from '../../actions/item'; import ItemsGrid from './ItemsGrid'; class Home extends Component { @@ -14,7 +17,10 @@ class Home extends Component { params: PropTypes.shape({ itemId: PropTypes.string }).isRequired, }).isRequired, activity: PropTypes.bool.isRequired, - rootItems: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + ownItems: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + sharedItems: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + t: PropTypes.func.isRequired, + dispatchGetSharedItems: PropTypes.func.isRequired, }; async componentDidMount() { @@ -23,23 +29,38 @@ class Home extends Component { } async componentDidUpdate() { - const { dispatchGetOwnItems, activity, rootItems } = this.props; + const { + dispatchGetOwnItems, + dispatchGetSharedItems, + activity, + ownItems, + sharedItems, + } = this.props; if (!activity) { // update dirty items - if (rootItems.some(({ dirty }) => dirty)) { + if (ownItems.some(({ dirty }) => dirty)) { dispatchGetOwnItems(); } + // update dirty items + if (sharedItems.some(({ dirty }) => dirty)) { + dispatchGetSharedItems(); + } } } render() { - const { rootItems } = this.props; + const { ownItems, sharedItems, t } = this.props; + return ( <> - + {t('My Items')} + + + {t('Items Shared With Me')} + ); } @@ -47,12 +68,16 @@ class Home extends Component { const mapStateToProps = ({ item }) => ({ activity: Object.values(item.get('activity').toJS()).flat().length, - rootItems: item.get('rootItems'), + ownItems: item.get('own'), + sharedItems: item.get('shared'), }); const mapDispatchToProps = { dispatchGetOwnItems: getOwnItems, + dispatchSetItem: setItem, + dispatchGetSharedItems: getSharedItems, }; const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)(Home); -export default withRouter(ConnectedComponent); +const TranslatedComponent = withTranslation()(ConnectedComponent); +export default withRouter(TranslatedComponent); diff --git a/src/components/main/Item.js b/src/components/main/Item.js index 131b8a95c..d3cd6470e 100644 --- a/src/components/main/Item.js +++ b/src/components/main/Item.js @@ -52,7 +52,7 @@ const Item = ({ item, dispatchDeleteItem }) => { dispatchDeleteItem(id)} + onClick={() => dispatchDeleteItem(item)} > diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js index 461bf9e2b..a47f7923c 100644 --- a/src/components/main/ItemMenu.js +++ b/src/components/main/ItemMenu.js @@ -10,6 +10,7 @@ import { setMoveModalSettings, setCopyModalSettings, setEditModalSettings, + setShareModalSettings, } from '../../actions/layout'; import { buildItemMenu, @@ -17,6 +18,7 @@ import { 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'; @@ -25,6 +27,7 @@ const ItemMenu = ({ dispatchSetMoveModalSettings, dispatchSetCopyModalSettings, dispatchSetEditModalSettings, + dispatchSetShareModalSettings, }) => { const [anchorEl, setAnchorEl] = React.useState(null); const { t } = useTranslation(); @@ -52,6 +55,11 @@ const ItemMenu = ({ handleClose(); }; + const handleShare = () => { + dispatchSetShareModalSettings({ open: true, itemId: item.id }); + handleClose(); + }; + return ( <> @@ -73,6 +81,12 @@ const ItemMenu = ({ {t('Copy')} + + {t('Share')} + ); @@ -85,12 +99,14 @@ ItemMenu.propTypes = { }).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/ItemScreen.js b/src/components/main/ItemScreen.js index a45b7f2e2..61f187c36 100644 --- a/src/components/main/ItemScreen.js +++ b/src/components/main/ItemScreen.js @@ -8,7 +8,12 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import { withRouter } from 'react-router'; import ItemsHeader from './ItemsHeader'; import NewItemButton from './NewItemButton'; -import { clearItem, setItem } from '../../actions/item'; +import { + clearItem, + getOwnItems, + setItem, + getSharedItems, +} from '../../actions/item'; import ItemsGrid from './ItemsGrid'; import { ITEM_SCREEN_ERROR_ALERT_ID } from '../../config/selectors'; import { areItemsEqual } from '../../utils/item'; @@ -23,6 +28,8 @@ class ItemScreen extends Component { t: PropTypes.func.isRequired, activity: PropTypes.bool, item: PropTypes.instanceOf(Map), + dispatchGetSharedItems: PropTypes.func.isRequired, + dispatchGetOwnItems: PropTypes.func.isRequired, }; static defaultProps = { @@ -41,6 +48,12 @@ class ItemScreen extends Component { dispatchSetItem(itemId); } + componentDidMount() { + const { dispatchGetOwnItems, dispatchGetSharedItems } = this.props; + dispatchGetOwnItems(); + dispatchGetSharedItems(); + } + shouldComponentUpdate({ item: nextItem, match: { @@ -125,6 +138,8 @@ const mapStateToProps = ({ item }) => ({ const mapDispatchToProps = { dispatchSetItem: setItem, dispatchClearItem: clearItem, + dispatchGetOwnItems: getOwnItems, + dispatchGetSharedItems: getSharedItems, }; const ConnectedComponent = connect( diff --git a/src/components/main/ItemsGrid.js b/src/components/main/ItemsGrid.js index eebb25c55..713306d9d 100644 --- a/src/components/main/ItemsGrid.js +++ b/src/components/main/ItemsGrid.js @@ -6,10 +6,7 @@ import { withTranslation } from 'react-i18next'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; 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 = { @@ -49,10 +46,6 @@ class ItemsGrid extends Component { {this.renderItems()} - - - - ); } diff --git a/src/components/main/ShareItemModal.js b/src/components/main/ShareItemModal.js new file mode 100644 index 000000000..e34ef7b40 --- /dev/null +++ b/src/components/main/ShareItemModal.js @@ -0,0 +1,162 @@ +import React from 'react'; +import { Map } from 'immutable'; +import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import TextField from '@material-ui/core/TextField'; +import Grid from '@material-ui/core/Grid'; +import Dialog from '@material-ui/core/Dialog'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Button from '@material-ui/core/Button'; +import { withStyles } from '@material-ui/core'; +import DialogActions from '@material-ui/core/DialogActions'; +import FormControl from '@material-ui/core/FormControl'; +import Select from '@material-ui/core/Select'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { shareItemWith } from '../../actions/membership'; +import { setShareModalSettings } from '../../actions/layout'; +import { + DEFAULT_PERMISSION_LEVEL, + PERMISSION_LEVELS, +} from '../../config/constants'; +import { + buildPermissionOptionId, + SHARE_ITEM_MODAL_EMAIL_INPUT_ID, + SHARE_ITEM_MODAL_PERMISSION_SELECT_ID, + SHARE_ITEM_MODAL_SHARE_BUTTON_ID, +} from '../../config/selectors'; + +const styles = (theme) => ({ + formControl: { + margin: theme.spacing(1), + minWidth: 120, + }, + dialogContent: { + display: 'flex', + flexDirection: 'column', + }, + shortInputField: { + width: '50%', + }, + addedMargin: { + marginTop: theme.spacing(2), + }, + emailInput: { + width: '100%', + }, +}); + +const ShareItemModal = ({ + dispatchSetShareModalSettings, + settings, + classes, +}) => { + const { t } = useTranslation(); + // refs + let email = ''; + let permission = ''; + + const handleClose = () => { + dispatchSetShareModalSettings({ open: false, itemId: null }); + }; + + const submit = () => { + // todo: check mail + const id = settings.get('itemId'); + shareItemWith({ + id, + email: email.value, + permission: permission.value, + }); + + handleClose(); + }; + + const labelId = 'permission-label'; + const renderPermissionSelect = () => ( + + {t('Permission')} + + + ); + + const open = settings.get('open'); + + return ( + + {t('Share Item')} + + + + { + email = c; + }} + label={t('Email')} + /> + + + {renderPermissionSelect()} + + + + + + + + + ); +}; + +ShareItemModal.propTypes = { + settings: PropTypes.instanceOf(Map).isRequired, + classes: PropTypes.shape({ + dialogContent: PropTypes.string.isRequired, + formControl: PropTypes.string.isRequired, + emailInput: PropTypes.string.isRequired, + }).isRequired, + dispatchSetShareModalSettings: PropTypes.func.isRequired, +}; + +const mapStateToProps = ({ layout }) => ({ + settings: layout.get('shareModal'), +}); + +const mapDispatchToProps = { + dispatchSetShareModalSettings: setShareModalSettings, +}; + +const ConnectedComponent = connect( + mapStateToProps, + mapDispatchToProps, +)(ShareItemModal); + +const StyledComponent = withStyles(styles)(ConnectedComponent); +export default StyledComponent; diff --git a/src/components/main/TreeModal.js b/src/components/main/TreeModal.js index 61003c4d3..6ebd7f759 100644 --- a/src/components/main/TreeModal.js +++ b/src/components/main/TreeModal.js @@ -17,6 +17,7 @@ import { getItem, getOwnItems, getItems, + getSharedItems, } from '../../actions/item'; import { ROOT_ID, @@ -66,6 +67,7 @@ class TreeModal extends Component { items: PropTypes.instanceOf(List).isRequired, dispatchGetItems: PropTypes.func.isRequired, dispatchGetOwnItems: PropTypes.func.isRequired, + dispatchGetShareItems: PropTypes.func.isRequired, }; static defaultProps = { @@ -75,8 +77,9 @@ class TreeModal extends Component { state = { selectedId: null, expandedItems: [ROOT_ID] }; componentDidMount() { - const { dispatchGetOwnItems } = this.props; + const { dispatchGetOwnItems, dispatchGetShareItems } = this.props; dispatchGetOwnItems(); + dispatchGetShareItems(); this.updateExpandedElements(); } @@ -246,7 +249,7 @@ class TreeModal extends Component { const mapStateToProps = ({ item }) => ({ items: item.get('items'), - rootItems: item.get('rootItems'), + rootItems: item.get('own'), }); const mapDispatchToProps = { @@ -254,6 +257,7 @@ const mapDispatchToProps = { dispatchGetItem: getItem, dispatchGetOwnItems: getOwnItems, dispatchGetChildren: getChildren, + dispatchGetShareItems: getSharedItems, }; const ConnectedComponent = connect( diff --git a/src/config/constants.js b/src/config/constants.js index ac0b51dfa..5f631c692 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -23,3 +23,15 @@ export const ITEM_TYPES = { APPLICATION: 'Application', EXERCISE: 'Exercise', }; +export const DRAWER_WIDTH = 300; +export const LOCALE_FORMAT = 'en-US'; + +export const PERMISSION_LEVELS = { + WRITE: 'write', + READ: 'read', + ADMIN: 'admin', +}; + +export const DEFAULT_PERMISSION_LEVEL = PERMISSION_LEVELS.WRITE; + +export const DEFAULT_CREATOR = '87c4940e-f764-4d86-8114-1edd9eff2ee0'; diff --git a/src/config/errors.js b/src/config/errors.js new file mode 100644 index 000000000..5f7739c98 --- /dev/null +++ b/src/config/errors.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const MEMBER_NOT_FOUND_ERROR = 'MEMBER_NOT_FOUND_ERROR'; diff --git a/src/config/selectors.js b/src/config/selectors.js index 37f0e3f29..65e7645d9 100644 --- a/src/config/selectors.js +++ b/src/config/selectors.js @@ -19,3 +19,9 @@ 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'; +export const ITEM_MENU_SHARE_BUTTON_CLASS = 'itemMenuShareButton'; +export const SHARE_ITEM_MODAL_EMAIL_INPUT_ID = 'shareItemModalEmailInput'; +export const SHARE_ITEM_MODAL_PERMISSION_SELECT_ID = + 'shareItemModalPermissionSelect'; +export const buildPermissionOptionId = (id) => `permission-${id}`; +export const SHARE_ITEM_MODAL_SHARE_BUTTON_ID = 'shareItemModalShareButton'; diff --git a/src/langs/en.json b/src/langs/en.json index 2ca0b5de8..c508e4635 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -35,6 +35,16 @@ "Edit Item": "Edit Item", "Edit": "Edit", "Add New Item": "Add New Item", - "Add Item": "Add Item" + "Add Item": "Add Item", + "Add new Item": "Ajouter un nouvel élément", + "No Item Selected": "Aucun élément sélectionné", + "Children": "Children", + "Extra": "Extra", + "Creator": "Creator", + "Created At": "Created At", + "Updated At": "Updated At", + "My Items": "My Items", + "Items Shared With Me": "Items Shared With Me", + "Home": "Home" } } diff --git a/src/langs/fr.json b/src/langs/fr.json index 28de4d114..a3201695d 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -35,6 +35,9 @@ "Edit Item": "Modifier l'élément", "Edit": "Modifier", "Add New Item": "Ajouter Un Nouvel Elément", - "Add Item": "Ajouter Un Elément" + "Add Item": "Ajouter Un Elément", + "My Items": "Mes éléments", + "Items Shared With Me": "Eléments partagés avec moi", + "Home": "Home" } } diff --git a/src/reducers/item.js b/src/reducers/item.js index 6522b1cbf..23bea23ad 100644 --- a/src/reducers/item.js +++ b/src/reducers/item.js @@ -22,6 +22,7 @@ import { FLAG_SETTING_ITEM, EDIT_ITEM_SUCCESS, FLAG_EDITING_ITEM, + GET_SHARED_ITEMS_SUCCESS, } from '../types/item'; const DEFAULT_ITEM = Map({ @@ -32,7 +33,8 @@ const DEFAULT_ITEM = Map({ const INITIAL_STATE = Map({ item: DEFAULT_ITEM, items: List(), - rootItems: List(), // items + shared: List(), + own: List(), // items activity: Map({ [FLAG_GETTING_ITEM]: [], [FLAG_CREATING_ITEM]: [], @@ -74,7 +76,7 @@ const updateInList = (els) => (list) => { return updateItemInList(els, list); }; -const removeFromList = (deletedItemId) => (list) => +const removeFromList = ({ id: deletedItemId }) => (list) => list.filter(({ id }) => id !== deletedItemId); export default (state = INITIAL_STATE, { type, payload }) => { @@ -110,7 +112,7 @@ export default (state = INITIAL_STATE, { type, payload }) => { const from = state.getIn(['item', 'id']); // add item in children or in root items if (!from) { - return state.update('rootItems', updateInList(payload)); + return state.update('own', updateInList(payload)); } return state.updateIn(['item', 'children'], updateInList(payload)); } @@ -119,7 +121,7 @@ export default (state = INITIAL_STATE, { type, payload }) => { const from = state.getIn(['item', 'id']); // delete item in children or in root items if (!from) { - return state.update('rootItems', removeFromList(payload)); + return state.update('own', removeFromList(payload)); } return state.updateIn(['item', 'children'], removeFromList(payload)); } @@ -130,18 +132,22 @@ export default (state = INITIAL_STATE, { type, payload }) => { return state.updateIn(['item', 'children'], updateInList(item)); } if (to === ROOT_ID) { - return state.updateIn(['rootItems'], updateInList(item)); + return state.updateIn(['own'], updateInList(item)); } return state; } case GET_CHILDREN_SUCCESS: { return state.updateIn(['items'], updateInList(payload.children)); } - case GET_OWN_ITEMS_SUCCESS: { + case GET_SHARED_ITEMS_SUCCESS: return state - .setIn(['rootItems'], List(payload)) + .setIn(['shared'], List(payload)) .updateIn(['items'], updateInList(payload)); - } + case GET_OWN_ITEMS_SUCCESS: + return state + .setIn(['own'], List(payload)) + .updateIn(['items'], updateInList(payload)); + case EDIT_ITEM_SUCCESS: { // update current elements if (state.getIn(['item', 'id'])) { @@ -149,7 +155,7 @@ export default (state = INITIAL_STATE, { type, payload }) => { } // update home elements - return state.updateIn(['rootItems'], updateInList(payload)); + return state.updateIn(['own'], updateInList(payload)); } default: return state; diff --git a/src/reducers/layout.js b/src/reducers/layout.js index 14607a311..d4eade73f 100644 --- a/src/reducers/layout.js +++ b/src/reducers/layout.js @@ -3,6 +3,7 @@ import { SET_MOVE_MODAL_SETTINGS, SET_COPY_MODAL_SETTINGS, SET_EDIT_MODAL_SETTINGS, + SET_SHARE_MODAL_SETTINGS, } from '../types/layout'; const INITIAL_STATE = Map({ @@ -18,6 +19,10 @@ const INITIAL_STATE = Map({ open: false, itemId: null, }), + shareModal: Map({ + open: false, + itemId: null, + }), }); export default (state = INITIAL_STATE, { type, payload }) => { @@ -28,6 +33,8 @@ export default (state = INITIAL_STATE, { type, payload }) => { return state.setIn(['moveModal'], Map(payload)); case SET_EDIT_MODAL_SETTINGS: return state.setIn(['editModal'], Map(payload)); + case SET_SHARE_MODAL_SETTINGS: + return state.setIn(['shareModal'], Map(payload)); default: return state; } diff --git a/src/types/item.js b/src/types/item.js index 3f4b68bb5..1c29edc94 100644 --- a/src/types/item.js +++ b/src/types/item.js @@ -19,3 +19,5 @@ 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'; +export const SET_SELECTED_ITEM_SUCCESS = 'SET_SELECTED_ITEM_SUCCESS'; +export const GET_SHARED_ITEMS_SUCCESS = 'GET_SHARED_ITEMS_SUCCESS'; diff --git a/src/types/layout.js b/src/types/layout.js index bbb2d3ec5..b5afb361b 100644 --- a/src/types/layout.js +++ b/src/types/layout.js @@ -1,3 +1,4 @@ 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'; +export const SET_SHARE_MODAL_SETTINGS = 'SET_SHARE_MODAL_SETTINGS'; From fd29189ddbbdf8029eaf77633631fb73cc980ca9 Mon Sep 17 00:00:00 2001 From: pyphilia Date: Wed, 3 Feb 2021 14:19:43 +0100 Subject: [PATCH 2/3] refactor: apply PR requested changes --- cypress/support/server.js | 35 ++++++++++++++------------------ cypress/support/utils.js | 3 --- package.json | 1 + src/api/item.js | 8 ++++---- src/api/member.js | 1 - src/api/membership.js | 13 ++++++------ src/api/routes.js | 2 +- src/components/main/TreeModal.js | 8 ++++---- src/config/constants.js | 4 +--- src/langs/en.json | 7 +++---- src/langs/fr.json | 12 ++++++++--- yarn.lock | 5 +++++ 12 files changed, 49 insertions(+), 50 deletions(-) diff --git a/cypress/support/server.js b/cypress/support/server.js index fb5de34ab..aee7280dc 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; +import { StatusCodes } from 'http-status-codes'; import { buildCopyItemRoute, buildDeleteItemRoute, @@ -18,13 +19,7 @@ import { transformIdForPath, } from '../../src/utils/item'; import { CURRENT_USER_ID } from '../fixtures/items'; -import { - ERROR_CODE, - ID_FORMAT, - parseStringToRegExp, - SUCCESS_CODE, - EMAIL_FORMAT, -} from './utils'; +import { ID_FORMAT, parseStringToRegExp, EMAIL_FORMAT } from './utils'; import { DEFAULT_PATCH, DEFAULT_GET, @@ -59,7 +54,7 @@ export const mockPostItem = (items, shouldThrowError) => { }, ({ body, reply }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE }); + return reply({ statusCode: StatusCodes.BAD_REQUEST }); } // add necessary properties id, path and creator @@ -82,12 +77,12 @@ export const mockDeleteItem = (items, shouldThrowError) => { }, ({ url, reply }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE, body: null }); + return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null }); } const id = url.slice(API_HOST.length).split('/')[2]; return reply({ - statusCode: SUCCESS_CODE, + statusCode: StatusCodes.OK, body: getItemById(items, id), }); }, @@ -102,14 +97,14 @@ export const mockGetItem = (items, shouldThrowError) => { }, ({ url, reply }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE, body: null }); + return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null }); } const id = url.slice(API_HOST.length).split('/')[2]; const item = getItemById(items, id); return reply({ body: item, - statusCode: SUCCESS_CODE, + statusCode: StatusCodes.OK, }); }, ).as('getItem'); @@ -137,7 +132,7 @@ export const mockMoveItem = (items, shouldThrowError) => { }, ({ url, reply, body }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE, body: null }); + return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null }); } const id = url.slice(API_HOST.length).split('/')[2]; @@ -152,7 +147,7 @@ export const mockMoveItem = (items, shouldThrowError) => { // todo: do for all children return reply({ - statusCode: SUCCESS_CODE, + statusCode: StatusCodes.OK, body: item, // this might not be accurate }); }, @@ -167,7 +162,7 @@ export const mockCopyItem = (items, shouldThrowError) => { }, ({ url, reply, body }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE, body: null }); + return reply({ statusCode: StatusCodes.BAD_REQUEST, body: null }); } const id = url.slice(API_HOST.length).split('/')[2]; @@ -184,7 +179,7 @@ export const mockCopyItem = (items, shouldThrowError) => { items.push(newItem); // todo: do for all children return reply({ - statusCode: SUCCESS_CODE, + statusCode: StatusCodes.OK, body: newItem, }); }, @@ -199,7 +194,7 @@ export const mockEditItem = (items, shouldThrowError) => { }, ({ reply, body }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE }); + return reply({ statusCode: StatusCodes.BAD_REQUEST }); } return reply(body); @@ -219,7 +214,7 @@ export const mockShareItem = (items, shouldThrowError) => { }, ({ reply, body }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE }); + return reply({ statusCode: StatusCodes.BAD_REQUEST }); } return reply(body); @@ -232,14 +227,14 @@ export const mockGetMember = (members, shouldThrowError) => { cy.intercept( { method: DEFAULT_GET.method, - pathname: MEMBERS_ROUTE, + pathname: `/${MEMBERS_ROUTE}`, query: { email: emailReg, }, }, ({ reply, url }) => { if (shouldThrowError) { - return reply({ statusCode: ERROR_CODE }); + return reply({ statusCode: StatusCodes.BAD_REQUEST }); } const mail = emailReg.exec(url)[0]; diff --git a/cypress/support/utils.js b/cypress/support/utils.js index cf136b84f..03d933c94 100644 --- a/cypress/support/utils.js +++ b/cypress/support/utils.js @@ -1,9 +1,6 @@ // use simple id format for tests export const ID_FORMAT = '[a-z0-9-]*'; -export const ERROR_CODE = 400; -export const SUCCESS_CODE = 200; - export const parseStringToRegExp = (string) => string.replaceAll('?', '\\?'); export const EMAIL_FORMAT = '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+'; diff --git a/package.json b/package.json index 5f5fefbc2..fccbe0d41 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "connected-react-router": "6.8.0", "dexie": "3.0.3", "history": "5.0.0", + "http-status-codes": "2.1.4", "i18next": "19.8.4", "immutable": "4.0.0-rc.12", "js-cookie": "2.2.1", diff --git a/src/api/item.js b/src/api/item.js index 8e2290ac1..72ef9edb6 100644 --- a/src/api/item.js +++ b/src/api/item.js @@ -164,13 +164,13 @@ export const copyItem = async ({ id, to }) => { }; export const getSharedItems = async () => { - const req = await fetch(`${API_HOST}/${SHARE_ITEM_WITH_ROUTE}`, { + const res = await fetch(`${API_HOST}/${SHARE_ITEM_WITH_ROUTE}`, { ...DEFAULT_GET, }); - if (req.status !== 200) { - throw new Error(req); + if (!res.ok) { + throw new Error(res); } - return req.json(); + return res.json(); }; diff --git a/src/api/member.js b/src/api/member.js index d15f74cda..326848846 100644 --- a/src/api/member.js +++ b/src/api/member.js @@ -3,7 +3,6 @@ import { API_HOST } from '../config/constants'; import { DEFAULT_GET } from './utils'; import { buildGetMemberBy, buildGetMember } from './routes'; -// eslint-disable-next-line import/prefer-default-export export const getMemberBy = async ({ email }) => { const res = await fetch(`${API_HOST}/${buildGetMemberBy(email)}`, { ...DEFAULT_GET, diff --git a/src/api/membership.js b/src/api/membership.js index 870f70d77..7221c78cc 100644 --- a/src/api/membership.js +++ b/src/api/membership.js @@ -8,28 +8,27 @@ import { import { MEMBER_NOT_FOUND_ERROR } from '../config/errors'; export const getMembershipsForItem = async ({ id }) => { - const req = await fetch( + const res = await fetch( `${API_HOST}/${buildGetItemMembershipForItemRoute(id)}`, DEFAULT_GET, ); - if (req.status !== 200) { - throw new Error(req); + if (!res.ok) { + throw new Error(res); } - return req.json(); + return res.json(); }; export const shareItemWith = async ({ id, email, permission }) => { const member = await getMemberBy({ email }); - // eslint-disable-next-line no-console if (!member) { throw new Error(MEMBER_NOT_FOUND_ERROR); } - const req = await fetch(`${API_HOST}/${buildShareItemWithRoute(id)}`, { + const res = await fetch(`${API_HOST}/${buildShareItemWithRoute(id)}`, { ...DEFAULT_POST, body: JSON.stringify({ memberId: member[0].id, permission }), // supposed to have only one member for this mail }); - return req.status === 200; + return res.ok; }; diff --git a/src/api/routes.js b/src/api/routes.js index 1ac336e46..ea7ec8d5b 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -17,6 +17,6 @@ export const buildShareItemWithRoute = (id) => `item-memberships?itemId=${id}`; export const buildGetItemMembershipForItemRoute = (id) => `item-memberships?itemId=${id}`; -export const MEMBERS_ROUTE = `/members`; +export const MEMBERS_ROUTE = `members`; export const buildGetMemberBy = (email) => `${MEMBERS_ROUTE}?email=${email}`; export const buildGetMember = (id) => `${MEMBERS_ROUTE}/${id}`; diff --git a/src/components/main/TreeModal.js b/src/components/main/TreeModal.js index 6ebd7f759..33ec071b0 100644 --- a/src/components/main/TreeModal.js +++ b/src/components/main/TreeModal.js @@ -67,7 +67,7 @@ class TreeModal extends Component { items: PropTypes.instanceOf(List).isRequired, dispatchGetItems: PropTypes.func.isRequired, dispatchGetOwnItems: PropTypes.func.isRequired, - dispatchGetShareItems: PropTypes.func.isRequired, + dispatchGetSharedItems: PropTypes.func.isRequired, }; static defaultProps = { @@ -77,9 +77,9 @@ class TreeModal extends Component { state = { selectedId: null, expandedItems: [ROOT_ID] }; componentDidMount() { - const { dispatchGetOwnItems, dispatchGetShareItems } = this.props; + const { dispatchGetOwnItems, dispatchGetSharedItems } = this.props; dispatchGetOwnItems(); - dispatchGetShareItems(); + dispatchGetSharedItems(); this.updateExpandedElements(); } @@ -257,7 +257,7 @@ const mapDispatchToProps = { dispatchGetItem: getItem, dispatchGetOwnItems: getOwnItems, dispatchGetChildren: getChildren, - dispatchGetShareItems: getSharedItems, + dispatchGetSharedItems: getSharedItems, }; const ConnectedComponent = connect( diff --git a/src/config/constants.js b/src/config/constants.js index 5f631c692..93fee866d 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -24,7 +24,7 @@ export const ITEM_TYPES = { EXERCISE: 'Exercise', }; export const DRAWER_WIDTH = 300; -export const LOCALE_FORMAT = 'en-US'; +export const DEFAULT_LOCALE = 'en-US'; export const PERMISSION_LEVELS = { WRITE: 'write', @@ -33,5 +33,3 @@ export const PERMISSION_LEVELS = { }; export const DEFAULT_PERMISSION_LEVEL = PERMISSION_LEVELS.WRITE; - -export const DEFAULT_CREATOR = '87c4940e-f764-4d86-8114-1edd9eff2ee0'; diff --git a/src/langs/en.json b/src/langs/en.json index c508e4635..c28c8f257 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -36,15 +36,14 @@ "Edit": "Edit", "Add New Item": "Add New Item", "Add Item": "Add Item", - "Add new Item": "Ajouter un nouvel élément", - "No Item Selected": "Aucun élément sélectionné", + "Add new Item": "Add new Item", + "No Item Selected": "No Item Selected", "Children": "Children", "Extra": "Extra", "Creator": "Creator", "Created At": "Created At", "Updated At": "Updated At", "My Items": "My Items", - "Items Shared With Me": "Items Shared With Me", - "Home": "Home" + "Items Shared With Me": "Items Shared With Me" } } diff --git a/src/langs/fr.json b/src/langs/fr.json index a3201695d..7d6c26046 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -28,16 +28,22 @@ "Owned Items": "Mes Éléments", "Type by author": "{{type}} par {{author}}", "Unknown": "Inconnu", - "Home": "Home", + "Home": "Accueil", "Add item": "Ajouter un élément", "Copy": "Copier", + "Add new Item": "Ajouter un nouvel élément", + "No Item Selected": "Aucun élément sélectionné", "Where do you want to copy this item?": "Où copier cet élément?", "Edit Item": "Modifier l'élément", "Edit": "Modifier", "Add New Item": "Ajouter Un Nouvel Elément", "Add Item": "Ajouter Un Elément", + "Children": "Enfants", + "Extra": "Extra", + "Creator": "Créateur", + "Created At": "Créé à", + "Updated At": "Mis à jour à", "My Items": "Mes éléments", - "Items Shared With Me": "Eléments partagés avec moi", - "Home": "Home" + "Items Shared With Me": "Eléments partagés avec moi" } } diff --git a/yarn.lock b/yarn.lock index 56453fe07..9da9ea7d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6639,6 +6639,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-status-codes@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" + integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" From 38a6c83e4f940dd4fbcb65c763c1610b3bddda3d Mon Sep 17 00:00:00 2001 From: pyphilia Date: Wed, 3 Feb 2021 14:51:09 +0100 Subject: [PATCH 3/3] refactor: add comment when fetching children --- src/actions/item.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/actions/item.js b/src/actions/item.js index 144c88681..d059dba73 100644 --- a/src/actions/item.js +++ b/src/actions/item.js @@ -232,6 +232,10 @@ export const getSharedItems = () => async (dispatch) => { const { children, id } = item; // get children let newChildren = []; + + // an item does not come automatically with children + // thus we need to fetch them if no children is specified + // we don't need to fetch again if they already exists (cache) if (!children) { // eslint-disable-next-line no-await-in-loop newChildren = await Api.getChildren(id);