Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add member validation alert #1350

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
spaenleh marked this conversation as resolved.
Show resolved Hide resolved
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