Skip to content

Commit

Permalink
feat: add member validation alert (#1350)
Browse files Browse the repository at this point in the history
* feat: add member validation alert

* fix: add test to check presence of the banner

* fix: remove empty useEffect in main component

* fix: apply review comments

* fix: allow legacy members to exist and don't show them the banner
  • Loading branch information
spaenleh authored Jul 22, 2024
1 parent bd6d322 commit 110bf1c
Show file tree
Hide file tree
Showing 21 changed files with 158 additions and 46 deletions.
31 changes: 31 additions & 0 deletions cypress/e2e/banners.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID,
MEMBER_VALIDATION_BANNER_ID,
} from '@/config/selectors';

import {
LEGACY_NOT_VALIDATED_MEMBER,
NOT_VALIDATED_MEMBER,
VALIDATED_MEMBER,
} from '../fixtures/members';

describe('Member validation banner', () => {
it('Shows banner when member is not validated', () => {
cy.setUpApi({ currentMember: NOT_VALIDATED_MEMBER });
cy.visit('/');
cy.get(`#${MEMBER_VALIDATION_BANNER_ID}`).should('be.visible');
cy.get(`#${MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID}`).click();
});

it('Does not show banner when member is validated', () => {
cy.setUpApi({ currentMember: VALIDATED_MEMBER });
cy.visit('/');
cy.get(`#${MEMBER_VALIDATION_BANNER_ID}`).should('not.exist');
});

it('Does not show banner when member is legacy', () => {
cy.setUpApi({ currentMember: LEGACY_NOT_VALIDATED_MEMBER });
cy.visit('/');
cy.get(`#${MEMBER_VALIDATION_BANNER_ID}`).should('not.exist');
});
});
10 changes: 10 additions & 0 deletions cypress/fixtures/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const MEMBERS: Record<string, MemberForTest> = {
lang: 'fr',
emailFreq: 'never',
},
isValidated: true,
}),
BOB: {
...MemberFactory({
Expand Down Expand Up @@ -80,10 +81,19 @@ export const MEMBERS: Record<string, MemberForTest> = {
email: '[email protected]',
createdAt: '2021-04-13 14:56:34.749946',
updatedAt: '2021-04-13 14:56:34.749946',
isValidated: false,
}),
};

export const CURRENT_USER = MEMBERS.ANNA;
export const NOT_VALIDATED_MEMBER = MEMBERS.GARRY;
export const VALIDATED_MEMBER = MEMBERS.ANNA;
export const LEGACY_NOT_VALIDATED_MEMBER = {
...NOT_VALIDATED_MEMBER,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
isValidated: undefined,
};

export const MOCK_SESSIONS = [
{ id: MEMBERS.BOB.id, token: 'bob-token', createdAt: Date.now() },
Expand Down
5 changes: 2 additions & 3 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
buildItemPath,
} from '../config/paths';
import { hooks } from '../config/queryClient';
import { useCurrentUserContext } from './context/CurrentUserContext';
import Main from './main/Main';
import Redirect from './main/Redirect';
import BookmarkedItemsScreen from './pages/BookmarkedItemsScreen';
Expand All @@ -34,11 +33,11 @@ import ItemSettingsPage from './pages/item/ItemSettingsPage';
import ItemSharingPage from './pages/item/ItemSharingPage';
import LibrarySettingsPage from './pages/item/LibrarySettingsPage';

const { useItemFeedbackUpdates } = hooks;
const { useItemFeedbackUpdates, useCurrentMember } = hooks;

const App = (): JSX.Element => {
const { pathname } = useLocation();
const { data: currentMember, isLoading } = useCurrentUserContext();
const { data: currentMember, isLoading } = useCurrentMember();

// registers the item updates through websockets
useItemFeedbackUpdates?.(currentMember?.id);
Expand Down
76 changes: 76 additions & 0 deletions src/components/alerts/MemberValidationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Link } from 'react-router-dom';

import {
Alert,
AlertTitle,
IconButton,
Link as MUILink,
Stack,
} from '@mui/material';

import { XIcon } from 'lucide-react';

import { useBuilderTranslation } from '@/config/i18n';
import { hooks } from '@/config/queryClient';
import {
MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID,
MEMBER_VALIDATION_BANNER_ID,
} from '@/config/selectors';
import { BUILDER } from '@/langs/constants';

import useModalStatus from '../hooks/useModalStatus';

const DOCUMENTATION_ORIGIN = 'https://graasp.github.io/docs';
const MEMBER_VALIDATION_DOCUMENTATION_LINK = '/user/account/validation';
const buildLocalizedDocumentationOrigin = (lang: string = 'en') => {
switch (lang) {
case 'en':
return DOCUMENTATION_ORIGIN;
default:
return `${DOCUMENTATION_ORIGIN}/${lang}`;
}
};
// eslint-disable-next-line arrow-body-style
const buildLocalizedDocumentationLink = (lang: string): string => {
// english does not use a path prefix
return `${buildLocalizedDocumentationOrigin(lang)}${MEMBER_VALIDATION_DOCUMENTATION_LINK}`;
};

const MemberValidationBanner = (): JSX.Element | false => {
const { isOpen, closeModal } = useModalStatus({
isInitiallyOpen: true,
});
const { t, i18n } = useBuilderTranslation();
const { data: member } = hooks.useCurrentMember();

// banner should not be shown when the member does not have the property
if (isOpen && member && 'isValidated' in member && !member.isValidated) {
return (
<Alert
id={MEMBER_VALIDATION_BANNER_ID}
severity="warning"
action={
<IconButton
id={MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID}
onClick={closeModal}
>
<XIcon />
</IconButton>
}
>
<AlertTitle>{t(BUILDER.MEMBER_VALIDATION_TITLE)}</AlertTitle>
<Stack gap={1}>
{t(BUILDER.MEMBER_VALIDATION_DESCRIPTION)}
<MUILink
component={Link}
to={buildLocalizedDocumentationLink(i18n.language)}
>
{t(BUILDER.MEMBER_VALIDATION_LINK_TEXT)}
</MUILink>
</Stack>
</Alert>
);
}
return false;
};
export default MemberValidationBanner;
3 changes: 1 addition & 2 deletions src/components/common/BookmarkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { useBuilderTranslation } from '../../config/i18n';
import { hooks, mutations } from '../../config/queryClient';
import { BUILDER } from '../../langs/constants';
import { useCurrentUserContext } from '../context/CurrentUserContext';

type Props = {
item: DiscriminatedItem;
Expand All @@ -29,7 +28,7 @@ const BookmarkButton = ({
type,
onClick,
}: Props): JSX.Element | null => {
const { data: member } = useCurrentUserContext();
const { data: member } = hooks.useCurrentMember();
const { data: bookmarks } = hooks.useBookmarkedItems();
const { t: translateBuilder } = useBuilderTranslation();
const addFavorite = mutations.useAddBookmarkedItem();
Expand Down
3 changes: 1 addition & 2 deletions src/components/common/Chatbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Loader } from '@graasp/ui';

import { hooks, mutations } from '../../config/queryClient';
import { CHATBOX_ID, CHATBOX_INPUT_BOX_ID } from '../../config/selectors';
import { useCurrentUserContext } from '../context/CurrentUserContext';

const { useItemChat, useAvatarUrl, useItemMemberships } = hooks;
const {
Expand All @@ -23,7 +22,7 @@ const Chatbox = ({ item }: Props): JSX.Element | null => {
useItemMemberships(item.id);
const members = itemPermissions?.map(({ member }) => member);
const { data: currentMember, isLoading: isLoadingCurrentMember } =
useCurrentUserContext();
hooks.useCurrentMember();
const { mutate: sendMessage } = usePostItemChatMessage();
const { mutate: editMessage } = usePatchItemChatMessage();
const { mutate: deleteMessage } = useDeleteItemChatMessage();
Expand Down
5 changes: 2 additions & 3 deletions src/components/common/UserSwitchWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UserSwitchWrapper as GraaspUserSwitch } from '@graasp/ui';

import { GRAASP_ACCOUNT_HOST, GRAASP_AUTH_HOST } from '@/config/env';
import { useBuilderTranslation } from '@/config/i18n';
import { mutations } from '@/config/queryClient';
import { hooks, mutations } from '@/config/queryClient';
import {
HEADER_MEMBER_MENU_BUTTON_ID,
HEADER_MEMBER_MENU_SEE_PROFILE_BUTTON_ID,
Expand All @@ -13,15 +13,14 @@ import {
} from '@/config/selectors';

import { BUILDER } from '../../langs/constants';
import { useCurrentUserContext } from '../context/CurrentUserContext';
import MemberAvatar from './MemberAvatar';

type Props = {
ButtonContent?: JSX.Element;
};

const UserSwitchWrapper = ({ ButtonContent }: Props): JSX.Element => {
const { data: member, isLoading } = useCurrentUserContext();
const { data: member, isLoading } = hooks.useCurrentMember();
const { t: translateBuilder } = useBuilderTranslation();
const { mutateAsync: signOut } = mutations.useSignOut();

Expand Down
21 changes: 6 additions & 15 deletions src/components/context/CurrentUserContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useMemo } from 'react';
import { ReactNode, createContext, useContext, useEffect } from 'react';

import i18n from '../../config/i18n';
import { hooks } from '../../config/queryClient';
Expand All @@ -12,30 +12,21 @@ const CurrentUserContext = createContext<CurrentUserContextType>(
);

type Props = {
children: JSX.Element | JSX.Element[];
children: ReactNode;
};

export const CurrentUserContextProvider = ({
children,
}: Props): JSX.Element => {
const query = useCurrentMember();
export const CurrentUserContextProvider = ({ children }: Props): ReactNode => {
const { data } = useCurrentMember();

// update language depending on user setting
const lang = query?.data?.extra?.lang;
const lang = data?.extra?.lang;
useEffect(() => {
if (lang !== i18n.language) {
i18n.changeLanguage(lang);
}
}, [lang]);

// eslint-disable-next-line react-hooks/exhaustive-deps
const value = useMemo(() => query, [query.data]);

return (
<CurrentUserContext.Provider value={value}>
{children}
</CurrentUserContext.Provider>
);
return children;
};

export const useCurrentUserContext = (): CurrentUserContextType =>
Expand Down
3 changes: 1 addition & 2 deletions src/components/item/ItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import {
buildFileItemId,
} from '../../config/selectors';
import ErrorAlert from '../common/ErrorAlert';
import { useCurrentUserContext } from '../context/CurrentUserContext';
import { OutletType } from '../pages/item/type';
import FolderContent from './FolderContent';
import FileAlignmentSetting from './settings/file/FileAlignmentSetting';
Expand Down Expand Up @@ -212,7 +211,7 @@ const EtherpadContent = ({ item }: { item: EtherpadItemType }): JSX.Element => {
* Main item renderer component
*/
const ItemContent = (): JSX.Element => {
const { data: member, isLoading, isError } = useCurrentUserContext();
const { data: member, isLoading, isError } = hooks.useCurrentMember();
const { item, permission } = useOutletContext<OutletType>();

if (isLoading) {
Expand Down
3 changes: 1 addition & 2 deletions src/components/item/ItemMemberships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ITEM_MEMBERSHIPS_CONTENT_ID } from '../../config/selectors';
import { BUILDER } from '../../langs/constants';
import { membershipsWithoutUser } from '../../utils/membership';
import MemberAvatar from '../common/MemberAvatar';
import { useCurrentUserContext } from '../context/CurrentUserContext';

type Props = {
id?: string;
Expand All @@ -19,7 +18,7 @@ type Props = {
const ItemMemberships = ({ id, maxAvatar = 2 }: Props): JSX.Element | null => {
const { t: translateBuilder } = useBuilderTranslation();
const { data: memberships, isError } = hooks.useItemMemberships(id);
const { data: currentUser } = useCurrentUserContext();
const { data: currentUser } = hooks.useCurrentMember();

if (!id) {
return null;
Expand Down
3 changes: 1 addition & 2 deletions src/components/item/publish/ItemPublishTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { PublicationStatus } from '@graasp/sdk';
import { Loader, theme } from '@graasp/ui';

import SyncIcon from '@/components/common/SyncIcon';
import { useCurrentUserContext } from '@/components/context/CurrentUserContext';
import {
DataSyncContextProvider,
useDataSyncContext,
Expand Down Expand Up @@ -36,7 +35,7 @@ const { usePublicationStatus } = hooks;
const ItemPublishTab = (): JSX.Element => {
const { t } = useBuilderTranslation();
const { item, canAdmin } = useOutletContext<OutletType>();
const { isLoading: isMemberLoading } = useCurrentUserContext();
const { isLoading: isMemberLoading } = hooks.useCurrentMember();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const { status } = useDataSyncContext();
const {
Expand Down
4 changes: 2 additions & 2 deletions src/components/item/settings/AdminChatSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { PackedItem, PermissionLevel } from '@graasp/sdk';
import { MessageSquareTextIcon } from 'lucide-react';

import { useBuilderTranslation } from '@/config/i18n';
import { hooks } from '@/config/queryClient';
import { BUILDER } from '@/langs/constants';

import { useCurrentUserContext } from '../../context/CurrentUserContext';
import ClearChatButton from './ClearChatButton';
import ItemSettingProperty from './ItemSettingProperty';

Expand All @@ -16,7 +16,7 @@ type Props = {
const AdminChatSettings = ({ item }: Props): JSX.Element | null => {
const itemId = item.id;
const { t } = useBuilderTranslation();
const { data: currentMember } = useCurrentUserContext();
const { data: currentMember } = hooks.useCurrentMember();
// only show export chat when user has admin right on the item
const isAdmin = currentMember
? item?.permission === PermissionLevel.Admin
Expand Down
6 changes: 2 additions & 4 deletions src/components/item/sharing/ConfirmMembership.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import {
} from '@graasp/sdk';
import { Button } from '@graasp/ui';

import { useCurrentUserContext } from '@/components/context/CurrentUserContext';

import { useBuilderTranslation } from '../../../config/i18n';
import { mutations } from '../../../config/queryClient';
import { hooks, mutations } from '../../../config/queryClient';
import { CONFIRM_MEMBERSHIP_DELETE_BUTTON_ID } from '../../../config/selectors';
import { BUILDER } from '../../../langs/constants';
import CancelButton from '../../common/CancelButton';
Expand All @@ -41,7 +39,7 @@ const DeleteItemDialog = ({
hasOnlyOneAdmin = false,
}: Props): JSX.Element => {
const { t: translateBuilder } = useBuilderTranslation();
const { data: member } = useCurrentUserContext();
const { data: member } = hooks.useCurrentMember();

const { mutate: deleteItemMembership } = mutations.useDeleteItemMembership();

Expand Down
5 changes: 3 additions & 2 deletions src/components/main/ItemForbiddenScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { Stack } from '@mui/material';

import { Button, ForbiddenContent } from '@graasp/ui';

import { hooks } from '@/config/queryClient';

import { useBuilderTranslation } from '../../config/i18n';
import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../config/selectors';
import { BUILDER } from '../../langs/constants';
import UserSwitchWrapper from '../common/UserSwitchWrapper';
import { useCurrentUserContext } from '../context/CurrentUserContext';

const ItemForbiddenScreen = (): JSX.Element => {
const { data: member } = useCurrentUserContext();
const { data: member } = hooks.useCurrentMember();
const { t: translateBuilder } = useBuilderTranslation();

const ButtonContent = (
Expand Down
2 changes: 2 additions & 0 deletions src/components/main/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
APP_NAVIGATION_PLATFORM_SWITCH_ID,
HEADER_APP_BAR_ID,
} from '../../config/selectors';
import MemberValidationBanner from '../alerts/MemberValidationBanner';
import CookiesBanner from '../common/CookiesBanner';
import UserSwitchWrapper from '../common/UserSwitchWrapper';
import MainMenu from './MainMenu';
Expand Down Expand Up @@ -101,6 +102,7 @@ const Main = ({ children }: Props): JSX.Element => {
/>
}
>
<MemberValidationBanner />
<CookiesBanner />
{children}
</GraaspMain>
Expand Down
Loading

0 comments on commit 110bf1c

Please sign in to comment.