From 92e37ad05a7c1bca262b538c1a2a41c9d5813978 Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 22 Oct 2021 10:38:17 +0200 Subject: [PATCH 1/5] refactor: add current user context --- src/components/App.js | 69 ++++++++++--------- src/components/common/Authorization.js | 6 +- src/components/common/Chatbox.js | 9 ++- src/components/common/SettingsHeader.js | 7 +- src/components/context/CurrentUserContext.js | 26 +++++++ .../context/FlagItemModalContext.js | 1 + src/components/item/ItemContent.js | 12 ++-- src/components/item/ItemMemberships.js | 5 +- .../item/header/ItemHeaderActions.js | 5 +- .../item/settings/ItemSettingsButton.js | 3 +- src/components/item/sharing/ItemSharingTab.js | 8 +-- .../item/sharing/VisibilitySelect.js | 9 ++- src/components/main/FavoriteItems.js | 5 +- src/components/main/Item.js | 7 +- src/components/main/ItemScreen.js | 5 +- src/components/main/ItemTypeTabs.js | 9 ++- src/components/main/MainMenu.js | 6 +- src/components/main/NewItemModal.js | 5 +- src/components/member/MemberProfileScreen.js | 6 +- src/components/table/ActionsCellRenderer.js | 7 +- 20 files changed, 130 insertions(+), 80 deletions(-) create mode 100644 src/components/context/CurrentUserContext.js diff --git a/src/components/App.js b/src/components/App.js index d73394760..dd83cee9b 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -35,6 +35,7 @@ import { ITEM_LOGIN_SIGN_IN_PASSWORD_ID, ITEM_LOGIN_SIGN_IN_USERNAME_ID, } from '../config/selectors'; +import { CurrentUserContextProvider } from './context/CurrentUserContext'; const App = () => { const { useCurrentMember, useItem, useItemLogin } = hooks; @@ -63,39 +64,41 @@ const App = () => { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; diff --git a/src/components/common/Authorization.js b/src/components/common/Authorization.js index cb07bc823..2611ad83e 100644 --- a/src/components/common/Authorization.js +++ b/src/components/common/Authorization.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { useLocation } from 'react-router'; import { API_ROUTES } from '@graasp/query-client'; import { @@ -6,10 +6,10 @@ import { REDIRECT_URL_LOCAL_STORAGE_KEY, NODE_ENV, } from '../../config/constants'; -import { hooks } from '../../config/queryClient'; import Loader from './Loader'; import RedirectPage from './RedirectionContent'; import { redirect } from '../../utils/navigation'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const Authorization = () => (ChildComponent) => { const ComposedComponent = (props) => { @@ -23,7 +23,7 @@ const Authorization = () => (ChildComponent) => { ); }; - const { data: currentMember, isLoading } = hooks.useCurrentMember(); + const { data: currentMember, isLoading } = useContext(CurrentUserContext); if (isLoading) { return ; diff --git a/src/components/common/Chatbox.js b/src/components/common/Chatbox.js index 3c21c494f..e5ebffc84 100644 --- a/src/components/common/Chatbox.js +++ b/src/components/common/Chatbox.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import GraaspChatbox from '@graasp/chatbox'; import { MUTATION_KEYS } from '@graasp/query-client'; @@ -7,15 +7,18 @@ import { Loader } from '@graasp/ui'; import { hooks, useMutation } from '../../config/queryClient'; import { HEADER_HEIGHT } from '../../config/constants'; import { CHATBOX_INPUT_BOX_ID, CHATBOX_ID } from '../../config/selectors'; +import { CurrentUserContext } from '../context/CurrentUserContext'; -const { useItemChat, useCurrentMember, useMembers } = hooks; +const { useItemChat, useMembers } = hooks; const Chatbox = ({ item }) => { const { data: chat, isLoading: isChatLoading } = useItemChat(item.get('id')); const { data: members, isLoading: isMembersLoading } = useMembers([ ...new Set(chat?.get('messages').map(({ creator }) => creator)), ]); - const { data: currentMember, isLoadingCurrentMember } = useCurrentMember(); + const { data: currentMember, isLoadingCurrentMember } = useContext( + CurrentUserContext, + ); const { mutate: sendMessage } = useMutation( MUTATION_KEYS.POST_ITEM_CHAT_MESSAGE, ); diff --git a/src/components/common/SettingsHeader.js b/src/components/common/SettingsHeader.js index 4c5a7ca26..4807105aa 100644 --- a/src/components/common/SettingsHeader.js +++ b/src/components/common/SettingsHeader.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Avatar from '@material-ui/core/Avatar'; @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import truncate from 'lodash.truncate'; import { MUTATION_KEYS, API_ROUTES } from '@graasp/query-client'; import MenuItem from '@material-ui/core/MenuItem'; -import { hooks, useMutation } from '../../config/queryClient'; +import { useMutation } from '../../config/queryClient'; import { AUTHENTICATION_HOST, USERNAME_MAX_LENGTH, @@ -22,6 +22,7 @@ import { } from '../../config/selectors'; import Loader from './Loader'; import { MEMBER_PROFILE_PATH } from '../../config/paths'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -38,7 +39,7 @@ const useStyles = makeStyles((theme) => ({ })); const SettingsHeader = () => { - const { data: user, isLoading } = hooks.useCurrentMember(); + const { data: user, isLoading } = useContext(CurrentUserContext); const classes = useStyles(); const { push } = useHistory(); const { t } = useTranslation(); diff --git a/src/components/context/CurrentUserContext.js b/src/components/context/CurrentUserContext.js new file mode 100644 index 000000000..879545597 --- /dev/null +++ b/src/components/context/CurrentUserContext.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { hooks } from '../../config/queryClient'; + +const CurrentUserContext = React.createContext(); + +const { useCurrentMember } = hooks; +const CurrentUserContextProvider = ({ children }) => { + const hook = useCurrentMember(); + + return ( + + {children} + + ); +}; + +CurrentUserContextProvider.propTypes = { + children: PropTypes.node, +}; + +CurrentUserContextProvider.defaultProps = { + children: null, +}; + +export { CurrentUserContext, CurrentUserContextProvider }; diff --git a/src/components/context/FlagItemModalContext.js b/src/components/context/FlagItemModalContext.js index 701962801..694905800 100644 --- a/src/components/context/FlagItemModalContext.js +++ b/src/components/context/FlagItemModalContext.js @@ -76,6 +76,7 @@ const FlagItemModalProvider = ({ children }) => { {flags?.map((flag) => ( ({ fileWrapper: { @@ -52,7 +48,9 @@ const ItemContent = ({ item, enableEdition }) => { const { editingItemId, setEditingItemId } = useContext(LayoutContext); // provide user to app - const { data: user, isLoading: isLoadingUser } = useCurrentMember(); + const { data: user, isLoading: isLoadingUser } = useContext( + CurrentUserContext, + ); // display children const { data: children, isLoading: isLoadingChildren } = useChildren(itemId, { diff --git a/src/components/item/ItemMemberships.js b/src/components/item/ItemMemberships.js index 1f2ba1c69..3ca996a1d 100644 --- a/src/components/item/ItemMemberships.js +++ b/src/components/item/ItemMemberships.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import EditIcon from '@material-ui/icons/Edit'; @@ -13,6 +13,7 @@ import MemberAvatar from '../common/MemberAvatar'; import { PERMISSION_LEVELS } from '../../enums'; import { ITEM_MEMBERSHIPS_CONTENT_ID } from '../../config/selectors'; import { membershipsWithoutUser } from '../../utils/membership'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const useStyles = makeStyles({ badge: { @@ -24,7 +25,7 @@ const ItemMemberships = ({ id, maxAvatar, onClick }) => { const { t } = useTranslation(); const classes = useStyles(); const { data: memberships, isError } = hooks.useItemMemberships(id); - const { data: currentUser } = hooks.useCurrentMember(); + const { data: currentUser } = useContext(CurrentUserContext); if (!id) { return null; diff --git a/src/components/item/header/ItemHeaderActions.js b/src/components/item/header/ItemHeaderActions.js index 938c628ec..e393f7678 100644 --- a/src/components/item/header/ItemHeaderActions.js +++ b/src/components/item/header/ItemHeaderActions.js @@ -24,6 +24,7 @@ import ItemSettingsButton from '../settings/ItemSettingsButton'; import PerformViewButton from '../../common/PerformViewButton'; import { isItemUpdateAllowedForUser } from '../../../utils/membership'; import { hooks } from '../../../config/queryClient'; +import { CurrentUserContext } from '../../context/CurrentUserContext'; const useStyles = makeStyles((theme) => ({ root: { @@ -37,7 +38,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const { useCurrentMember, useItemMemberships } = hooks; +const { useItemMemberships } = hooks; const ItemHeaderActions = ({ onClickMetadata, onClickChatbox, item }) => { const classes = useStyles(); @@ -51,7 +52,7 @@ const ItemHeaderActions = ({ onClickMetadata, onClickChatbox, item }) => { const id = item?.get('id'); const type = item?.get('type'); - const { data: member } = useCurrentMember(); + const { data: member } = useContext(CurrentUserContext); const { data: memberships } = useItemMemberships(item.get('id')); const canEdit = isItemUpdateAllowedForUser({ diff --git a/src/components/item/settings/ItemSettingsButton.js b/src/components/item/settings/ItemSettingsButton.js index 7c8b76231..61924e00f 100644 --- a/src/components/item/settings/ItemSettingsButton.js +++ b/src/components/item/settings/ItemSettingsButton.js @@ -12,13 +12,14 @@ import { buildSettingsButtonId, ITEM_SETTINGS_BUTTON_CLASS, } from '../../../config/selectors'; +import { CurrentUserContext } from '../../context/CurrentUserContext'; function ItemSettingsButton({ id }) { const { setIsItemSettingsOpen, isItemSettingsOpen } = useContext( LayoutContext, ); const { data: memberships } = hooks.useItemMemberships(id); - const { data: user, isError } = hooks.useCurrentMember(); + const { data: user, isError } = useContext(CurrentUserContext); const memberId = user?.get('id'); const { t } = useTranslation(); diff --git a/src/components/item/sharing/ItemSharingTab.js b/src/components/item/sharing/ItemSharingTab.js index 0c7f0a1f8..6585a01a5 100644 --- a/src/components/item/sharing/ItemSharingTab.js +++ b/src/components/item/sharing/ItemSharingTab.js @@ -18,6 +18,7 @@ import { import { PSEUDONIMIZED_USER_MAIL } from '../../../config/constants'; import { getItemLoginSchema } from '../../../utils/itemExtra'; import { LayoutContext } from '../../context/LayoutContext'; +import { CurrentUserContext } from '../../context/CurrentUserContext'; const useStyles = makeStyles((theme) => ({ title: { @@ -40,10 +41,9 @@ const ItemSharingTab = ({ item }) => { data: memberships, isLoading: isMembershipsLoading, } = hooks.useItemMemberships(id); - const { - data: currentMember, - isLoadingCurrentMember, - } = hooks.useCurrentMember(); + const { data: currentMember, isLoadingCurrentMember } = useContext( + CurrentUserContext, + ); const canEdit = isItemUpdateAllowedForUser({ memberships, memberId: currentMember?.get('id'), diff --git a/src/components/item/sharing/VisibilitySelect.js b/src/components/item/sharing/VisibilitySelect.js index d272a9e71..bd2919c4a 100644 --- a/src/components/item/sharing/VisibilitySelect.js +++ b/src/components/item/sharing/VisibilitySelect.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Loader } from '@graasp/ui'; import MenuItem from '@material-ui/core/MenuItem'; @@ -20,10 +20,11 @@ import { SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, SHARE_ITEM_VISIBILITY_SELECT_ID, } from '../../../config/selectors'; +import { CurrentUserContext } from '../../context/CurrentUserContext'; const { DELETE_ITEM_TAG, POST_ITEM_TAG, PUT_ITEM_LOGIN } = MUTATION_KEYS; -const { useTags, useItemTags, useCurrentMember, useItemLogin } = hooks; +const { useTags, useItemTags, useItemLogin } = hooks; const useStyles = makeStyles({ loginSchemaText: { @@ -35,7 +36,9 @@ function VisibilitySelect({ item, edit }) { const { t } = useTranslation(); const classes = useStyles(); // user - const { data: user, isLoading: isMemberLoading } = useCurrentMember(); + const { data: user, isLoading: isMemberLoading } = useContext( + CurrentUserContext, + ); // current item const { itemId } = useParams(); diff --git a/src/components/main/FavoriteItems.js b/src/components/main/FavoriteItems.js index c02b34faf..3a4beff16 100644 --- a/src/components/main/FavoriteItems.js +++ b/src/components/main/FavoriteItems.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { List } from 'immutable'; import { useTranslation } from 'react-i18next'; import { MUTATION_KEYS } from '@graasp/query-client'; @@ -18,6 +18,7 @@ import { getExistingItems, } from '../../utils/item'; import { getFavoriteItems } from '../../utils/member'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const FavoriteItems = () => { const { t } = useTranslation(); @@ -25,7 +26,7 @@ const FavoriteItems = () => { data: member, isLoading: isMemberLoading, isError: isMemberError, - } = hooks.useCurrentMember(); + } = useContext(CurrentUserContext); const { data: favoriteItems = List(), isLoading: isItemsLoading, diff --git a/src/components/main/Item.js b/src/components/main/Item.js index 2bbca1603..ceecaee73 100644 --- a/src/components/main/Item.js +++ b/src/components/main/Item.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import truncate from 'lodash.truncate'; @@ -16,8 +16,9 @@ import { getItemImage, stripHtml } from '../../utils/item'; import FavoriteButton from '../common/FavoriteButton'; import { hooks } from '../../config/queryClient'; import PinButton from '../common/PinButton'; +import { CurrentUserContext } from '../context/CurrentUserContext'; -const { useCurrentMember, useItemMemberships } = hooks; +const { useItemMemberships } = hooks; const useStyles = makeStyles(() => ({ root: { @@ -35,7 +36,7 @@ const Item = ({ item }) => { const image = getItemImage(item); - const { data: member } = useCurrentMember(); + const { data: member } = useContext(CurrentUserContext); const { data: memberships } = useItemMemberships(id); const enableEdition = isItemUpdateAllowedForUser({ memberships, diff --git a/src/components/main/ItemScreen.js b/src/components/main/ItemScreen.js index 284458d8b..4f43b2114 100644 --- a/src/components/main/ItemScreen.js +++ b/src/components/main/ItemScreen.js @@ -3,6 +3,7 @@ import { useParams } from 'react-router'; import { hooks } from '../../config/queryClient'; import { isItemUpdateAllowedForUser } from '../../utils/membership'; import ErrorAlert from '../common/ErrorAlert'; +import { CurrentUserContext } from '../context/CurrentUserContext'; import { LayoutContext } from '../context/LayoutContext'; import ItemContent from '../item/ItemContent'; import ItemMain from '../item/ItemMain'; @@ -10,7 +11,7 @@ import ItemSettings from '../item/settings/ItemSettings'; import ItemSharingTab from '../item/sharing/ItemSharingTab'; import Main from './Main'; -const { useItem, useCurrentMember, useItemMemberships } = hooks; +const { useItem, useItemMemberships } = hooks; const ItemScreen = () => { const { itemId } = useParams(); @@ -23,7 +24,7 @@ const ItemScreen = () => { setIsItemSharingOpen, isItemSharingOpen, } = useContext(LayoutContext); - const { data: currentMember } = useCurrentMember(); + const { data: currentMember } = useContext(CurrentUserContext); const { data: memberships } = useItemMemberships(itemId); const enableEdition = isItemUpdateAllowedForUser({ memberships, diff --git a/src/components/main/ItemTypeTabs.js b/src/components/main/ItemTypeTabs.js index 07e6f3320..4cfa5e217 100644 --- a/src/components/main/ItemTypeTabs.js +++ b/src/components/main/ItemTypeTabs.js @@ -27,11 +27,11 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ItemTypeTabs = ({ onTypeChange }) => { +const ItemTypeTabs = ({ onTypeChange, initialValue }) => { const classes = useStyles(); const { t } = useTranslation(); - const [value, setValue] = React.useState(ITEM_TYPES.FOLDER); + const [value, setValue] = React.useState(initialValue ?? ITEM_TYPES.FOLDER); const handleChange = (event, newValue) => { setValue(newValue); @@ -87,6 +87,11 @@ const ItemTypeTabs = ({ onTypeChange }) => { ItemTypeTabs.propTypes = { onTypeChange: PropTypes.func.isRequired, + initialValue: PropTypes.oneOf(Object.values(ITEM_TYPES)), +}; + +ItemTypeTabs.defaultProps = { + initialValue: null, }; export default ItemTypeTabs; diff --git a/src/components/main/MainMenu.js b/src/components/main/MainMenu.js index 3da8c2791..544b4c00c 100644 --- a/src/components/main/MainMenu.js +++ b/src/components/main/MainMenu.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import FolderSharedIcon from '@material-ui/icons/FolderShared'; import ListItem from '@material-ui/core/ListItem'; import PollIcon from '@material-ui/icons/Poll'; @@ -10,20 +10,20 @@ import DeleteIcon from '@material-ui/icons/Delete'; import { useLocation, useHistory } from 'react-router'; import List from '@material-ui/core/List'; import FavoriteIcon from '@material-ui/icons/Favorite'; -import { hooks } from '../../config/queryClient'; import { FAVORITE_ITEMS_PATH, HOME_PATH, SHARED_ITEMS_PATH, RECYCLE_BIN_PATH, } from '../../config/paths'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const MainMenu = () => { const { t } = useTranslation(); const [dense] = useState(true); const { push } = useHistory(); const { pathname } = useLocation(); - const { data: member } = hooks.useCurrentMember(); + const { data: member } = useContext(CurrentUserContext); const goTo = (path) => { push(path); diff --git a/src/components/main/NewItemModal.js b/src/components/main/NewItemModal.js index 68bde5663..9011bf9ae 100644 --- a/src/components/main/NewItemModal.js +++ b/src/components/main/NewItemModal.js @@ -174,7 +174,10 @@ const NewItemModal = ({ open, handleClose }) => { return ( - + {renderContent()} {renderActions()} diff --git a/src/components/member/MemberProfileScreen.js b/src/components/member/MemberProfileScreen.js index 9a983faf5..d4e2c7c22 100644 --- a/src/components/member/MemberProfileScreen.js +++ b/src/components/member/MemberProfileScreen.js @@ -1,11 +1,10 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Grid, IconButton, makeStyles, Typography } from '@material-ui/core'; import { Loader } from '@graasp/ui'; import FileCopyIcon from '@material-ui/icons/FileCopy'; import Card from '@material-ui/core/Card'; import { useTranslation } from 'react-i18next'; import { ReactComponent as GraaspLogo } from '../../resources/graasp-logo.svg'; -import { hooks } from '../../config/queryClient'; import LanguageSwitch from './LanguageSwitch'; import { formatDate } from '../../utils/date'; import { DEFAULT_LANG } from '../../config/constants'; @@ -21,6 +20,7 @@ import { import notifier from '../../middlewares/notifier'; import { COPY_MEMBER_ID_TO_CLIPBOARD } from '../../types/clipboard'; import Main from '../main/Main'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const useStyles = makeStyles((theme) => ({ profileTable: { @@ -36,7 +36,7 @@ const useStyles = makeStyles((theme) => ({ const MemberProfileScreen = () => { const { t } = useTranslation(); const classes = useStyles(); - const { data: member, isLoading } = hooks.useCurrentMember(); + const { data: member, isLoading } = useContext(CurrentUserContext); if (isLoading) { return ; diff --git a/src/components/table/ActionsCellRenderer.js b/src/components/table/ActionsCellRenderer.js index 8a348307f..e3fe464d0 100644 --- a/src/components/table/ActionsCellRenderer.js +++ b/src/components/table/ActionsCellRenderer.js @@ -1,16 +1,17 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useContext } from 'react'; import EditButton from '../common/EditButton'; import ItemMenu from '../main/ItemMenu'; import { hooks } from '../../config/queryClient'; import FavoriteButton from '../common/FavoriteButton'; import PinButton from '../common/PinButton'; import { isItemUpdateAllowedForUser } from '../../utils/membership'; +import { CurrentUserContext } from '../context/CurrentUserContext'; -const { useCurrentMember, useItemMemberships } = hooks; +const { useItemMemberships } = hooks; const ActionsCellRenderer = ({ data: item }) => { - const { data: member } = useCurrentMember(); + const { data: member } = useContext(CurrentUserContext); const { data: memberships } = useItemMemberships(item.id); const canEdit = isItemUpdateAllowedForUser({ From 24f6c9471f51fed80c76dbe9024c6513baae6743 Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 22 Oct 2021 11:27:31 +0200 Subject: [PATCH 2/5] refactor: handle loading app lists with skeleton --- src/components/item/form/AppForm.js | 51 ++++++++++++++++------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/item/form/AppForm.js b/src/components/item/form/AppForm.js index 6279613af..141d858b9 100644 --- a/src/components/item/form/AppForm.js +++ b/src/components/item/form/AppForm.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Typography from '@material-ui/core/Typography'; import { makeStyles, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; +import Skeleton from '@material-ui/lab/Skeleton'; import BaseItemForm from './BaseItemForm'; import { buildAppExtra, getAppExtra } from '../../../utils/itemExtra'; import { hooks } from '../../../config/queryClient'; @@ -30,7 +31,7 @@ const AppForm = ({ onChange, item }) => { const classes = useStyles(); const { useApps } = hooks; - const { data } = useApps(); + const { data, isLoading: isAppsLoading } = useApps(); const url = getAppExtra(item?.extra)?.url; @@ -39,27 +40,33 @@ const AppForm = ({ onChange, item }) => { {t('Create an App')} - option.name ?? option} - value={url} - onChange={handleAppUrlInput} - onInputChange={handleAppUrlInput} - renderOption={(option) => ( -
- {option.name} - {option.name} -
- )} - // eslint-disable-next-line react/jsx-props-no-spreading - renderInput={(params) => } - /> + {isAppsLoading ? ( + + ) : ( + option.name ?? option} + value={url} + onChange={handleAppUrlInput} + onInputChange={handleAppUrlInput} + renderOption={(option) => ( +
+ {option.name} + {option.name} +
+ )} + renderInput={(params) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )} + /> + )} ); }; From acb94793d7cedea28f5bcb585ed44797d7a5f28f Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 22 Oct 2021 13:39:02 +0200 Subject: [PATCH 3/5] refactor: fix language switch depending on user setting --- src/components/context/CurrentUserContext.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/context/CurrentUserContext.js b/src/components/context/CurrentUserContext.js index 879545597..ec71ede82 100644 --- a/src/components/context/CurrentUserContext.js +++ b/src/components/context/CurrentUserContext.js @@ -1,15 +1,24 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { hooks } from '../../config/queryClient'; +import i18n from '../../config/i18n'; const CurrentUserContext = React.createContext(); const { useCurrentMember } = hooks; const CurrentUserContextProvider = ({ children }) => { - const hook = useCurrentMember(); + const query = useCurrentMember(); + + // update language depending on user setting + const lang = query?.data?.get('extra')?.lang; + useEffect(() => { + if (lang !== i18n.language) { + i18n.changeLanguage(lang); + } + }, [lang]); return ( - + {children} ); From 7d7c9a2144c871d59b26444b392684854757ae9a Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 22 Oct 2021 15:37:51 +0200 Subject: [PATCH 4/5] refactor: optimize use of item memberships --- .../integration/item/create/createApp.spec.js | 22 ++--- cypress/support/commands.js | 5 +- cypress/support/server.js | 80 ++++++++++++------- package.json | 2 +- src/components/common/FavoriteButton.js | 8 +- src/components/item/ItemContent.js | 2 + src/components/item/ItemMemberships.js | 6 +- .../item/header/ItemHeaderActions.js | 6 +- .../item/settings/ItemSettingsButton.js | 11 --- .../item/sharing/ItemMembershipsTable.js | 5 +- src/components/item/sharing/ItemSharingTab.js | 11 +-- src/components/main/CustomCardHeader.js | 5 +- src/components/main/Item.js | 9 +-- src/components/main/ItemMenu.js | 19 ++--- src/components/main/ItemScreen.js | 6 +- src/components/main/Items.js | 22 +++++ src/components/main/ItemsGrid.js | 16 +++- src/components/main/ItemsTable.js | 14 +++- src/components/table/ActionsCellRenderer.js | 76 +++++++++++++----- src/config/constants.js | 1 + src/utils/membership.js | 5 ++ yarn.lock | 12 +-- 22 files changed, 216 insertions(+), 127 deletions(-) diff --git a/cypress/integration/item/create/createApp.spec.js b/cypress/integration/item/create/createApp.spec.js index e659f8141..304b3a353 100644 --- a/cypress/integration/item/create/createApp.spec.js +++ b/cypress/integration/item/create/createApp.spec.js @@ -8,7 +8,7 @@ import { createItem } from './utils'; describe('Create App', () => { describe('create app on Home', () => { - it('Create app on Home with dropdown', () =>{ + it('Create app on Home with dropdown', () => { cy.setUpApi(); cy.visit(HOME_PATH); @@ -27,7 +27,7 @@ describe('Create App', () => { }); }); - it('Create app on Home by typing', () =>{ + it('Create app on Home by typing', () => { cy.setUpApi(); cy.visit(HOME_PATH); @@ -51,37 +51,37 @@ describe('Create App', () => { it('Create app with dropdown', () => { cy.setUpApi(SAMPLE_ITEMS); const { id } = SAMPLE_ITEMS.items[0]; - + // go to children item cy.visit(buildItemPath(id)); - + if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } - + // create createItem(GRAASP_APP_ITEM, ITEM_LAYOUT_MODES.LIST); - + cy.wait('@postItem').then(() => { // expect update cy.wait('@getItem').its('response.url').should('contain', id); }); }); - + it('Create app by typing', () => { cy.setUpApi(SAMPLE_ITEMS); const { id } = SAMPLE_ITEMS.items[0]; - + // go to children item cy.visit(buildItemPath(id)); - + if (DEFAULT_ITEM_LAYOUT_MODE !== ITEM_LAYOUT_MODES.LIST) { cy.switchMode(ITEM_LAYOUT_MODES.LIST); } - + // create createItem(GRAASP_APP_ITEM, { type: true }); - + cy.wait('@postItem').then(() => { // expect update cy.wait('@getItem').its('response.url').should('contain', id); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index cfde5b168..62fcead18 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -62,6 +62,7 @@ import { mockGetRecycledItems, mockDeleteItemTag, mockRestoreItems, + mockGetMembers, } from './server'; import './commands/item'; import './commands/navigation'; @@ -143,6 +144,8 @@ Cypress.Commands.add( mockGetMember(cachedMembers); + mockGetMembers(cachedMembers); + mockGetMemberBy(cachedMembers, getMemberError); mockUploadItem(cachedItems, defaultUploadError); @@ -179,7 +182,7 @@ Cypress.Commands.add( mockEditItemMembershipForItem(items); - mockDeleteItemMembershipForItem(items); + mockDeleteItemMembershipForItem(); mockGetFlags(flags); diff --git a/cypress/support/server.js b/cypress/support/server.js index f2a8f568d..05d7ec073 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -59,7 +59,7 @@ const { SIGN_OUT_ROUTE, buildPostItemLoginSignInRoute, buildGetItemLoginRoute, - buildGetItemMembershipsForItemRoute, + buildGetItemMembershipsForItemsRoute, buildGetItemTagsRoute, GET_TAGS_ROUTE, buildPutItemLoginSchema, @@ -75,6 +75,8 @@ const { buildRecycleItemRoute, GET_RECYCLED_ITEMS_ROUTE, buildDeleteItemTagRoute, + buildDeleteItemsRoute, + buildGetMembersRoute, } = API_ROUTES; const API_HOST = Cypress.env('API_HOST'); @@ -109,7 +111,6 @@ export const mockGetAppListRoute = (apps) => { ).as('getApps'); }; - export const mockGetCurrentMember = ( currentMember = MEMBERS.ANNA, shouldThrowError = false, @@ -222,8 +223,7 @@ export const mockDeleteItems = (items, shouldThrowError) => { cy.intercept( { method: DEFAULT_DELETE.method, - pathname: `/${ITEMS_ROUTE}`, - query: { id: new RegExp(ID_FORMAT) }, + url: new RegExp(`${API_HOST}/${buildDeleteItemsRoute([])}`), }, ({ url, reply }) => { const ids = qs.parse(url.slice(url.indexOf('?') + 1)).id; @@ -640,6 +640,32 @@ export const mockGetMember = (members) => { }, ).as('getMember'); }; +export const mockGetMembers = (members) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: `${API_HOST}/${buildGetMembersRoute([''])}`, + }, + ({ url, reply }) => { + let { id: memberIds } = qs.parse(url.slice(url.indexOf('?') + 1)); + if (typeof memberIds === 'string') { + memberIds = [memberIds]; + } + const allMembers = memberIds?.map((id) => getMemberById(members, id)); + // member does not exist in db + if (!allMembers) { + return reply({ + statusCode: StatusCodes.NOT_FOUND, + }); + } + + return reply({ + body: allMembers, + statusCode: StatusCodes.OK, + }); + }, + ).as('getMembers'); +}; export const mockGetMemberBy = (members, shouldThrowError) => { cy.intercept( @@ -876,29 +902,29 @@ export const mockGetItemMembershipsForItem = (items) => { method: DEFAULT_GET.method, url: new RegExp( `${API_HOST}/${parseStringToRegExp( - buildGetItemMembershipsForItemRoute(ID_FORMAT), - )}$`, + buildGetItemMembershipsForItemsRoute([]), + )}`, ), }, ({ reply, url }) => { const { itemId } = qs.parse(url.slice(url.indexOf('?') + 1)); - const item = items.find(({ id }) => id === itemId); - if (!item) { - return reply([]); - } - const result = item.memberships || [ - { - permission: PERMISSION_LEVELS.ADMIN, - memberId: item.creator, - itemId: item.id, - }, - ]; - return reply(result); + const selectedItems = items.filter(({ id }) => itemId.includes(id)); + const allMemberships = selectedItems.map( + ({ creator, id, memberships }) => + memberships || [ + { + permission: PERMISSION_LEVELS.ADMIN, + memberId: creator, + itemId: id, + }, + ], + ); + reply(allMemberships); }, ).as('getItemMemberships'); }; -export const mockEditItemMembershipForItem = (items) => { +export const mockEditItemMembershipForItem = () => { cy.intercept( { method: DEFAULT_PATCH.method, @@ -906,15 +932,14 @@ export const mockEditItemMembershipForItem = (items) => { `${API_HOST}/${buildEditItemMembershipRoute(ID_FORMAT)}$`, ), }, - ({ reply, url }) => { - const mId = url.slice(API_HOST.length).split('/')[2]; - const result = items.find(({ id }) => id === mId)?.memberships || []; - reply(result?.find(({ id }) => id === mId)); + ({ reply }) => { + // this mock intercept does nothing + reply(true); }, ).as('editItemMembership'); }; -export const mockDeleteItemMembershipForItem = (items) => { +export const mockDeleteItemMembershipForItem = () => { cy.intercept( { method: DEFAULT_DELETE.method, @@ -922,10 +947,9 @@ export const mockDeleteItemMembershipForItem = (items) => { `${API_HOST}/${buildDeleteItemMembershipRoute(ID_FORMAT)}$`, ), }, - ({ reply, url }) => { - const mId = url.slice(API_HOST.length).split('/')[2]; - const result = items.find(({ id }) => id === mId)?.memberships || []; - reply(result?.find(({ id }) => id === mId)); + ({ reply }) => { + // this mock intercept does nothing + reply(true); }, ).as('deleteItemMembership'); }; diff --git a/package.json b/package.json index 6cb25abc1..069795d49 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "AGPL-3.0-only", "dependencies": { "@graasp/chatbox": "git://github.com/graasp/graasp-chatbox.git#main", - "@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#main", + "@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#82/updateDeleteItemsRoute", "@graasp/ui": "git://github.com/graasp/graasp-ui.git#master", "@material-ui/core": "4.11.2", "@material-ui/icons": "5.0.0-beta.4", diff --git a/src/components/common/FavoriteButton.js b/src/components/common/FavoriteButton.js index 02586ed83..3cd1562ac 100644 --- a/src/components/common/FavoriteButton.js +++ b/src/components/common/FavoriteButton.js @@ -1,7 +1,6 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import IconButton from '@material-ui/core/IconButton'; -import { Map } from 'immutable'; import FavoriteIcon from '@material-ui/icons/Favorite'; import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'; import { useTranslation } from 'react-i18next'; @@ -10,8 +9,10 @@ import { MUTATION_KEYS } from '@graasp/query-client'; import { FAVORITE_ITEM_BUTTON_CLASS } from '../../config/selectors'; import { useMutation } from '../../config/queryClient'; import { isItemFavorite } from '../../utils/item'; +import { CurrentUserContext } from '../context/CurrentUserContext'; -const FavoriteButton = ({ item, member }) => { +const FavoriteButton = ({ item }) => { + const { data: member } = useContext(CurrentUserContext); const { t } = useTranslation(); const mutation = useMutation(MUTATION_KEYS.EDIT_MEMBER); @@ -60,7 +61,6 @@ const FavoriteButton = ({ item, member }) => { FavoriteButton.propTypes = { item: PropTypes.shape({ id: PropTypes.string.isRequired }).isRequired, - member: PropTypes.instanceOf(Map).isRequired, }; export default FavoriteButton; diff --git a/src/components/item/ItemContent.js b/src/components/item/ItemContent.js index df6b15397..e6ab1c79a 100644 --- a/src/components/item/ItemContent.js +++ b/src/components/item/ItemContent.js @@ -175,6 +175,8 @@ const ItemContent = ({ item, enableEdition }) => { id={buildItemsTableId(itemId)} title={item.get('name')} items={children} + isEditing={isEditing} + onSaveCaption={onSaveCaption} headerElements={ enableEdition ? [] : undefined } diff --git a/src/components/item/ItemMemberships.js b/src/components/item/ItemMemberships.js index 3ca996a1d..87d0a3bf3 100644 --- a/src/components/item/ItemMemberships.js +++ b/src/components/item/ItemMemberships.js @@ -24,7 +24,7 @@ const useStyles = makeStyles({ const ItemMemberships = ({ id, maxAvatar, onClick }) => { const { t } = useTranslation(); const classes = useStyles(); - const { data: memberships, isError } = hooks.useItemMemberships(id); + const { data: memberships, isError } = hooks.useItemMemberships([id]); const { data: currentUser } = useContext(CurrentUserContext); if (!id) { @@ -36,12 +36,12 @@ const ItemMemberships = ({ id, maxAvatar, onClick }) => { } const filteredMemberships = membershipsWithoutUser( - memberships, + memberships?.get(0), currentUser?.get('id'), ); // display only if has more than 2 memberships - if (filteredMemberships.isEmpty()) { + if (!filteredMemberships.length) { return null; } diff --git a/src/components/item/header/ItemHeaderActions.js b/src/components/item/header/ItemHeaderActions.js index e393f7678..348f226fb 100644 --- a/src/components/item/header/ItemHeaderActions.js +++ b/src/components/item/header/ItemHeaderActions.js @@ -54,9 +54,9 @@ const ItemHeaderActions = ({ onClickMetadata, onClickChatbox, item }) => { const { data: member } = useContext(CurrentUserContext); - const { data: memberships } = useItemMemberships(item.get('id')); + const { data: memberships } = useItemMemberships([id]); const canEdit = isItemUpdateAllowedForUser({ - memberships, + memberships: memberships?.get(0), memberId: member?.get('id'), }); @@ -93,7 +93,7 @@ const ItemHeaderActions = ({ onClickMetadata, onClickChatbox, item }) => { return ( <> {!isItemSettingsOpen && activeActions} - + {canEdit && } ); } diff --git a/src/components/item/settings/ItemSettingsButton.js b/src/components/item/settings/ItemSettingsButton.js index 61924e00f..dd889ab2b 100644 --- a/src/components/item/settings/ItemSettingsButton.js +++ b/src/components/item/settings/ItemSettingsButton.js @@ -5,22 +5,16 @@ import Tooltip from '@material-ui/core/Tooltip'; import SettingsIcon from '@material-ui/icons/Settings'; import { useTranslation } from 'react-i18next'; import CloseIcon from '@material-ui/icons/Close'; -import { hooks } from '../../../config/queryClient'; import { LayoutContext } from '../../context/LayoutContext'; -import { isSettingsEditionAllowedForUser } from '../../../utils/membership'; import { buildSettingsButtonId, ITEM_SETTINGS_BUTTON_CLASS, } from '../../../config/selectors'; -import { CurrentUserContext } from '../../context/CurrentUserContext'; function ItemSettingsButton({ id }) { const { setIsItemSettingsOpen, isItemSettingsOpen } = useContext( LayoutContext, ); - const { data: memberships } = hooks.useItemMemberships(id); - const { data: user, isError } = useContext(CurrentUserContext); - const memberId = user?.get('id'); const { t } = useTranslation(); // on unmount close item settings @@ -32,11 +26,6 @@ function ItemSettingsButton({ id }) { [], ); - // settings are not available for user without edition membership - if (isError || !isSettingsEditionAllowedForUser({ memberships, memberId })) { - return null; - } - const onClickSettings = () => { setIsItemSettingsOpen(!isItemSettingsOpen); }; diff --git a/src/components/item/sharing/ItemMembershipsTable.js b/src/components/item/sharing/ItemMembershipsTable.js index 3abdec864..290445983 100644 --- a/src/components/item/sharing/ItemMembershipsTable.js +++ b/src/components/item/sharing/ItemMembershipsTable.js @@ -8,7 +8,6 @@ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import TableContainer from '@material-ui/core/TableContainer'; import { useTranslation } from 'react-i18next'; -import { List } from 'immutable'; import TableRow from '@material-ui/core/TableRow'; import { MUTATION_KEYS } from '@graasp/query-client'; import IconButton from '@material-ui/core/IconButton'; @@ -96,7 +95,7 @@ const ItemMembershipsTable = ({ memberships, id, emptyMessage }) => { const classes = useStyles(); const { t } = useTranslation(); - const content = memberships.size ? ( + const content = memberships.length ? ( memberships.map((row) => ) ) : ( @@ -115,7 +114,7 @@ const ItemMembershipsTable = ({ memberships, id, emptyMessage }) => { ItemMembershipsTable.propTypes = { id: PropTypes.string.isRequired, - memberships: PropTypes.instanceOf(List).isRequired, + memberships: PropTypes.arrayOf(PropTypes.shape({})).isRequired, emptyMessage: PropTypes.string, }; diff --git a/src/components/item/sharing/ItemSharingTab.js b/src/components/item/sharing/ItemSharingTab.js index 6585a01a5..9306ddc5d 100644 --- a/src/components/item/sharing/ItemSharingTab.js +++ b/src/components/item/sharing/ItemSharingTab.js @@ -33,14 +33,9 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ItemSharingTab = ({ item }) => { +const ItemSharingTab = ({ item, memberships }) => { const { t } = useTranslation(); const classes = useStyles(); - const id = item.get('id'); - const { - data: memberships, - isLoading: isMembershipsLoading, - } = hooks.useItemMemberships(id); const { data: currentMember, isLoadingCurrentMember } = useContext( CurrentUserContext, ); @@ -61,7 +56,7 @@ const ItemSharingTab = ({ item }) => { [], ); - if (isMembershipsLoading || isLoadingCurrentMember) { + if (isLoadingCurrentMember) { return ; } @@ -75,7 +70,6 @@ const ItemSharingTab = ({ item }) => { memberships, currentMember.get('id'), ); - const authorizedMemberships = membershipsWithoutSelf.filter( ({ memberId }) => { const member = members?.find(({ id: mId }) => mId === memberId); @@ -140,5 +134,6 @@ const ItemSharingTab = ({ item }) => { }; ItemSharingTab.propTypes = { item: PropTypes.instanceOf(Map).isRequired, + memberships: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; export default ItemSharingTab; diff --git a/src/components/main/CustomCardHeader.js b/src/components/main/CustomCardHeader.js index fa0add512..8a97298e0 100644 --- a/src/components/main/CustomCardHeader.js +++ b/src/components/main/CustomCardHeader.js @@ -40,7 +40,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const CustomCardHeader = ({ item }) => { +const CustomCardHeader = ({ item, canEdit }) => { const { id, creator, name, type } = item; const classes = useStyles(); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const CustomCardHeader = ({ item }) => { - + ); }; @@ -78,6 +78,7 @@ CustomCardHeader.propTypes = { name: PropTypes.string.isRequired, type: PropTypes.string.isRequired, }).isRequired, + canEdit: PropTypes.bool.isRequired, }; export default CustomCardHeader; diff --git a/src/components/main/Item.js b/src/components/main/Item.js index ceecaee73..ad7b677c0 100644 --- a/src/components/main/Item.js +++ b/src/components/main/Item.js @@ -14,12 +14,9 @@ import EditButton from '../common/EditButton'; import { isItemUpdateAllowedForUser } from '../../utils/membership'; import { getItemImage, stripHtml } from '../../utils/item'; import FavoriteButton from '../common/FavoriteButton'; -import { hooks } from '../../config/queryClient'; import PinButton from '../common/PinButton'; import { CurrentUserContext } from '../context/CurrentUserContext'; -const { useItemMemberships } = hooks; - const useStyles = makeStyles(() => ({ root: { maxWidth: 345, @@ -30,14 +27,13 @@ const useStyles = makeStyles(() => ({ }, })); -const Item = ({ item }) => { +const Item = ({ item, memberships }) => { const classes = useStyles(); const { id, name, description } = item; const image = getItemImage(item); const { data: member } = useContext(CurrentUserContext); - const { data: memberships } = useItemMemberships(id); const enableEdition = isItemUpdateAllowedForUser({ memberships, memberId: member?.get('id'), @@ -45,7 +41,7 @@ const Item = ({ item }) => { return ( - + @@ -76,6 +72,7 @@ Item.propTypes = { image: PropTypes.string.isRequired, }).isRequired, }).isRequired, + memberships: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; export default Item; diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js index 5579582c3..e382c0b73 100644 --- a/src/components/main/ItemMenu.js +++ b/src/components/main/ItemMenu.js @@ -1,7 +1,6 @@ import IconButton from '@material-ui/core/IconButton'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; -import { Map } from 'immutable'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import PropTypes from 'prop-types'; import { MUTATION_KEYS } from '@graasp/query-client'; @@ -21,17 +20,9 @@ import { CopyItemModalContext } from '../context/CopyItemModalContext'; import { CreateShortcutModalContext } from '../context/CreateShortcutModalContext'; import { MoveItemModalContext } from '../context/MoveItemModalContext'; import { FlagItemModalContext } from '../context/FlagItemModalContext'; -import { useMutation, hooks } from '../../config/queryClient'; -import { isItemUpdateAllowedForUser } from '../../utils/membership'; +import { useMutation } from '../../config/queryClient'; -const { useItemMemberships } = hooks; - -const ItemMenu = ({ item, member }) => { - const { data: memberships } = useItemMemberships(item.id); - const canEdit = isItemUpdateAllowedForUser({ - memberships, - memberId: member?.get('id'), - }); +const ItemMenu = ({ item, canEdit }) => { const [anchorEl, setAnchorEl] = React.useState(null); const { t } = useTranslation(); const { mutate: recycleItem } = useMutation(MUTATION_KEYS.RECYCLE_ITEM); @@ -129,7 +120,11 @@ const ItemMenu = ({ item, member }) => { ItemMenu.propTypes = { item: PropTypes.shape({ id: PropTypes.string.isRequired }).isRequired, - member: PropTypes.instanceOf(Map).isRequired, + canEdit: PropTypes.bool, +}; + +ItemMenu.defaultProps = { + canEdit: false, }; export default ItemMenu; diff --git a/src/components/main/ItemScreen.js b/src/components/main/ItemScreen.js index 4f43b2114..31fc32aba 100644 --- a/src/components/main/ItemScreen.js +++ b/src/components/main/ItemScreen.js @@ -25,9 +25,9 @@ const ItemScreen = () => { isItemSharingOpen, } = useContext(LayoutContext); const { data: currentMember } = useContext(CurrentUserContext); - const { data: memberships } = useItemMemberships(itemId); + const { data: memberships } = useItemMemberships([itemId]); const enableEdition = isItemUpdateAllowedForUser({ - memberships, + memberships: memberships?.get(0), memberId: currentMember?.get('id'), }); @@ -47,7 +47,7 @@ const ItemScreen = () => { return ; } if (isItemSharingOpen) { - return ; + return ; } return ; })(); diff --git a/src/components/main/Items.js b/src/components/main/Items.js index c68a4c4ce..a39bd9b6f 100644 --- a/src/components/main/Items.js +++ b/src/components/main/Items.js @@ -1,12 +1,16 @@ import { List } from 'immutable'; import PropTypes from 'prop-types'; +import { Loader } from '@graasp/ui'; import React, { useContext } from 'react'; import { ITEM_LAYOUT_MODES } from '../../enums'; import { LayoutContext } from '../context/LayoutContext'; import { useItemSearch } from '../item/ItemSearch'; import ItemsGrid from './ItemsGrid'; +import { hooks } from '../../config/queryClient'; import ItemsTable from './ItemsTable'; +const { useItemMemberships } = hooks; + const Items = ({ items, title, @@ -16,9 +20,21 @@ const Items = ({ toolbarActions, clickable, defautSortedColumn, + isEditing, + onSaveCaption, }) => { const { mode } = useContext(LayoutContext); const itemSearch = useItemSearch(items); + const { + data: memberships, + isLoading: isMembershipsLoading, + } = useItemMemberships( + itemSearch?.results?.map(({ id: itemId }) => itemId).toJS(), + ); + + if (isMembershipsLoading) { + return ; + } switch (mode) { case ITEM_LAYOUT_MODES.GRID: @@ -27,6 +43,7 @@ const Items = ({ id={id} title={title} items={itemSearch.results} + memberships={memberships} // This enables the possiblity to display messages (item is empty, no search result) itemSearch={itemSearch} headerElements={[itemSearch.input, ...headerElements]} @@ -41,11 +58,14 @@ const Items = ({ id={id} tableTitle={title} items={itemSearch.results} + memberships={memberships} headerElements={[itemSearch.input, ...headerElements]} isSearching={Boolean(itemSearch.text)} actions={actions} toolbarActions={toolbarActions} clickable={clickable} + isEditing={isEditing} + onSaveCaption={onSaveCaption} /> ); } @@ -65,6 +85,8 @@ Items.propTypes = { type: PropTypes.string, name: PropTypes.string, }), + isEditing: PropTypes.bool.isRequired, + onSaveCaption: PropTypes.func.isRequired, }; Items.defaultProps = { diff --git a/src/components/main/ItemsGrid.js b/src/components/main/ItemsGrid.js index 9668aced5..832fd1da6 100644 --- a/src/components/main/ItemsGrid.js +++ b/src/components/main/ItemsGrid.js @@ -15,6 +15,7 @@ import { ITEMS_GRID_ITEMS_PER_PAGE_SELECT_LABEL_ID, ITEMS_GRID_PAGINATION_ID, } from '../../config/selectors'; +import { getMembershipsForItem } from '../../utils/membership'; import { ItemSearchInput, NoItemSearchResult } from '../item/ItemSearch'; import EmptyItem from './EmptyItem'; import Item from './Item'; @@ -46,7 +47,14 @@ const styles = (theme) => ({ }); const ItemsGrid = (props) => { - const { classes, items, title, itemSearch, headerElements } = props; + const { + classes, + items, + title, + itemSearch, + headerElements, + memberships, + } = props; const { t } = useTranslation(); const [page, setPage] = useState(1); @@ -80,7 +88,10 @@ const ItemsGrid = (props) => { return itemsInPage.map((item) => ( - + )); }; @@ -126,6 +137,7 @@ const ItemsGrid = (props) => { ItemsGrid.propTypes = { items: PropTypes.instanceOf(List).isRequired, + memberships: PropTypes.instanceOf(List).isRequired, match: PropTypes.shape({ params: PropTypes.shape({ itemId: PropTypes.string }).isRequired, }).isRequired, diff --git a/src/components/main/ItemsTable.js b/src/components/main/ItemsTable.js index 2f74850a9..820d9bb62 100644 --- a/src/components/main/ItemsTable.js +++ b/src/components/main/ItemsTable.js @@ -1,6 +1,6 @@ import { List } from 'immutable'; import PropTypes from 'prop-types'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { AgGridColumn, AgGridReact } from 'ag-grid-react'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-material.css'; @@ -26,6 +26,7 @@ import { buildItemsTableRowId } from '../../config/selectors'; import NameCellRenderer from '../table/NameCellRenderer'; import DragCellRenderer from '../table/DragCellRenderer'; import ActionsCellRenderer from '../table/ActionsCellRenderer'; +import { CurrentUserContext } from '../context/CurrentUserContext'; const { useItem } = hooks; @@ -54,6 +55,7 @@ const useStyles = makeStyles((theme) => ({ const ItemsTable = ({ items: rows, + memberships, tableTitle, id: tableId, headerElements, @@ -68,6 +70,7 @@ const ItemsTable = ({ const classes = useStyles(); const { itemId } = useParams(); const { data: parentItem } = useItem(itemId); + const { data: member } = useContext(CurrentUserContext); const [gridApi, setGridApi] = useState(null); const [selected, setSelected] = useState([]); @@ -140,6 +143,11 @@ const ItemsTable = ({ const NoRowsComponent = () => {t('No items')}; const parentDescription = parentItem?.get('description'); + const ActionComponent = ActionsCellRenderer({ + memberships, + items: rows, + member, + }); return (
{ + const ChildComponent = ({ data: item }) => { + const [canEdit, setCanEdit] = useState(false); -const ActionsCellRenderer = ({ data: item }) => { - const { data: member } = useContext(CurrentUserContext); + useEffect(() => { + if (items && memberships && !memberships.isEmpty() && !items.isEmpty()) { + setCanEdit( + isItemUpdateAllowedForUser({ + memberships: getMembershipsForItem({ item, items, memberships }), + memberId: member?.get('id'), + }), + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items, memberships, item, member]); - const { data: memberships } = useItemMemberships(item.id); - const canEdit = isItemUpdateAllowedForUser({ - memberships, - memberId: member?.get('id'), - }); + const renderAnyoneActions = () => ( + + ); - return ( - <> - {!member.isEmpty() && } - {canEdit && ( + const renderAuthenticatedActions = () => { + if (!member || member.isEmpty()) { + return null; + } + + return ; + }; + + const renderEditorActions = () => { + if (!canEdit) { + return null; + } + + return ( <> - )} - - - ); + ); + }; + + return ( + <> + {renderAuthenticatedActions()} + {renderEditorActions()} + {renderAnyoneActions()} + + ); + }; + ChildComponent.propTypes = { + data: PropTypes.shape({}).isRequired, + }; + return ChildComponent; }; ActionsCellRenderer.propTypes = { - data: PropTypes.shape({}).isRequired, + memberships: PropTypes.instanceOf(List).isRequired, + member: PropTypes.instanceOf(Map).isRequired, }; export default ActionsCellRenderer; diff --git a/src/config/constants.js b/src/config/constants.js index cdf2ffcea..0102b04fc 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -136,6 +136,7 @@ export const COMPOSE_VIEW_SELECTION = 'composeViewSelection'; export const PERFORM_VIEW_SELECTION = 'performViewSelection'; export const ITEM_TYPES_WITH_CAPTIONS = [ + ITEM_TYPES.FOLDER, ITEM_TYPES.S3_FILE, ITEM_TYPES.FILE, ITEM_TYPES.APP, diff --git a/src/utils/membership.js b/src/utils/membership.js index 098b07e4f..092849121 100644 --- a/src/utils/membership.js +++ b/src/utils/membership.js @@ -17,3 +17,8 @@ export const isItemUpdateAllowedForUser = ({ memberships, memberId }) => export const membershipsWithoutUser = (memberships, userId) => memberships.filter(({ memberId }) => memberId !== userId); + +export const getMembershipsForItem = ({ item, memberships, items }) => { + const index = items.findKey(({ id }) => id === item.id); + return memberships.get(index); +}; diff --git a/yarn.lock b/yarn.lock index 78876b19e..6b8b8d99b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2155,9 +2155,9 @@ __metadata: languageName: node linkType: hard -"@graasp/query-client@git://github.com/graasp/graasp-query-client.git#main": +"@graasp/query-client@git://github.com/graasp/graasp-query-client.git#82/updateDeleteItemsRoute": version: 0.1.0 - resolution: "@graasp/query-client@git://github.com/graasp/graasp-query-client.git#commit=73c3de4b659a623ae0e493af36208a7403a7d14d" + resolution: "@graasp/query-client@git://github.com/graasp/graasp-query-client.git#commit=2f2ebf632221cddc90028c0d08e57bfbf628c8f3" dependencies: axios: 0.21.4 http-status-codes: 2.1.4 @@ -2168,13 +2168,13 @@ __metadata: uuid: 8.3.2 peerDependencies: react: ^17.0.0 - checksum: d0d4513a3bac65d72f5fba7b330bf6a5aed0c7f491d3cbe12267586d6f5c3cc5aeda27554f842489d893375237fc72d0c78eac5bf3b2be035926830b188b95c5 + checksum: f267436328ca25c9d22c6c2a471c1652c73229f5b4a77ae3393bf07aa099b4ebcffba238192f2a18fa9acc3d3cb22f4b3ffeb14b4d9f4313ae017ef16aa6e7cc languageName: node linkType: hard "@graasp/ui@git://github.com/graasp/graasp-ui.git#master": version: 0.2.0 - resolution: "@graasp/ui@git://github.com/graasp/graasp-ui.git#commit=4690448335e127b8dcf14be5c39574a437a9b5f4" + resolution: "@graasp/ui@git://github.com/graasp/graasp-ui.git#commit=692915e6d935f37ef6f490bc178c60abdb2a5fc7" dependencies: clsx: 1.1.1 http-status-codes: 2.1.4 @@ -2191,7 +2191,7 @@ __metadata: i18next: 21.3.1 react: ^16.13.1 react-dom: 16.13.1 - checksum: 5221c537009d8a936e725a95fb88af6efe676ec50a2a75e2ab2718edb37a85da1080f50298b5b084247c509ea0a5a27b5b56bbafc84b33a5824180c528aed6b6 + checksum: 63fc7e878982336ebcdb04cb5b2abc62e1b00b45740063905eda5554889e686659fcab919c66396ac34402ef24c8bf1512ed4a1cb0ae5b07b0cd217ccfdd9cdf languageName: node linkType: hard @@ -10240,7 +10240,7 @@ fsevents@^1.2.7: "@cypress/code-coverage": 3.9.2 "@cypress/instrument-cra": 1.4.0 "@graasp/chatbox": "git://github.com/graasp/graasp-chatbox.git#main" - "@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#main" + "@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#82/updateDeleteItemsRoute" "@graasp/ui": "git://github.com/graasp/graasp-ui.git#master" "@graasp/websockets": "git://github.com/graasp/graasp-websockets.git#master" "@material-ui/core": 4.11.2 From e0fad1462801ba60a2717cf64e857df47b2f1656 Mon Sep 17 00:00:00 2001 From: pyphilia Date: Fri, 29 Oct 2021 10:22:55 +0200 Subject: [PATCH 5/5] refactor: fix public tests --- cypress/integration/item/view/utils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cypress/integration/item/view/utils.js b/cypress/integration/item/view/utils.js index 781fe15ed..05ed25631 100644 --- a/cypress/integration/item/view/utils.js +++ b/cypress/integration/item/view/utils.js @@ -49,9 +49,6 @@ export const expectItemHeaderLayout = ({ }) => { const header = cy.get(`#${ITEM_HEADER_ID}`); - if (ITEM_TYPES_WITH_CAPTIONS.includes(type)) { - header.get(`#${buildEditButtonId(id)}`).should('exist'); - } header.get(`#${buildShareButtonId(id)}`).should('exist'); header.get(`#${buildPerformButtonId(id)}`).should('exist'); @@ -60,8 +57,12 @@ export const expectItemHeaderLayout = ({ memberships, memberId: currentMember?.id, }) - ) + ) { + if (ITEM_TYPES_WITH_CAPTIONS.includes(type)) { + header.get(`#${buildEditButtonId(id)}`).should('exist'); + } header.get(`#${buildSettingsButtonId(id)}`).should('exist'); + } }; export const expectDocumentViewScreenLayout = ({