diff --git a/src/Authorization/PreventGuestWrapper.stories.tsx b/src/Authorization/PreventGuestWrapper.stories.tsx new file mode 100644 index 000000000..8eb64889a --- /dev/null +++ b/src/Authorization/PreventGuestWrapper.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from '@storybook/test'; + +import { AccountType, CompleteGuest, CompleteMember } from '@graasp/sdk'; + +import BuildIcon from '@/icons/BuildIcon.js'; + +import PreventGuestWrapper from './PreventGuestWrapper.js'; + +const meta = { + title: 'Actions/PreventGuestWrapper', + component: PreventGuestWrapper, + + argTypes: {}, + args: { + buttonText: 'Log out and Create a Graasp account', + errorText: 'An error occured.', + text: 'You are currently using Graasp with a guest account. In order to use all features of Graasp, you have to log out and create a Graasp account.', + children: ( +
+ + + +
+ ), + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Guest = { + args: { + currentAccount: { type: AccountType.Guest } as CompleteGuest, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // should see message + await expect(canvas.getByRole('alert')).toBeVisible(); + }, +} satisfies Story; + +export const Individual = { + args: { + currentAccount: { id: 'member', name: 'member' } as CompleteMember, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // should see content + await expect(canvas.getByTestId('content')).toBeVisible(); + }, +} satisfies Story; + +export const ShowError = { + args: { + errorText: 'error text', + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + // should see error message + if (args.errorText) { + await expect(canvas.getByText(args.errorText)).toBeVisible(); + } + }, +} satisfies Story; diff --git a/src/Authorization/PreventGuestWrapper.tsx b/src/Authorization/PreventGuestWrapper.tsx new file mode 100644 index 000000000..4a785285a --- /dev/null +++ b/src/Authorization/PreventGuestWrapper.tsx @@ -0,0 +1,73 @@ +import { ClipboardPen } from 'lucide-react'; + +import { + Alert, + Box, + Container, + Button as MuiButton, + Stack, + Typography, +} from '@mui/material'; + +import { ReactNode } from 'react'; + +import { AccountType, CurrentAccount } from '@graasp/sdk'; + +type Props = { + buttonText: string; + children?: ReactNode; + currentAccount?: CurrentAccount | null; + /** + * Component to display on error. + * Overrides errorText + */ + error?: JSX.Element; + errorText: string; + id?: string; + onButtonClick?: () => void; + startIcon?: JSX.Element; + text: string | JSX.Element; +}; + +const PreventGuestWrapper = ({ + buttonText, + children, + currentAccount, + error, + id, + onButtonClick, + startIcon = , + errorText, + text, +}: Props): ReactNode => { + if (currentAccount) { + // guest - should not have access to children + if (currentAccount.type === AccountType.Guest) { + return ( + + + + {text} + + + {buttonText} + + + + + + ); + } + + return children; + } + + return error ?? {errorText}; +}; + +export default PreventGuestWrapper; diff --git a/src/Authorization/Redirect.stories.tsx b/src/Authorization/Redirect.stories.tsx index 2be1dbe73..606beca21 100644 --- a/src/Authorization/Redirect.stories.tsx +++ b/src/Authorization/Redirect.stories.tsx @@ -3,19 +3,13 @@ import { expect, within } from '@storybook/test'; import { BrowserRouter } from 'react-router-dom'; -import BuildIcon from '../icons/BuildIcon.js'; -import withAuthorization from './withAuthorization.js'; - -const ComponentWithAuthorization = withAuthorization(BuildIcon, { - // use an empty string because we do not want to be redirected but the prop is mandatory - redirectionLink: '', -}); +import SignedInWrapper from './SignedInWrapper.js'; // this story is separated from the others // because the redirection breaks a bit the navigation in storybook -const meta: Meta = { +const meta: Meta = { title: 'Actions/Authorization/Redirect', - component: ComponentWithAuthorization, + component: SignedInWrapper, parameters: { docs: { source: { @@ -24,6 +18,11 @@ const meta: Meta = { }, }, }, + render: () => ( + +
+ + ), decorators: [ (story) => { return {story()}; @@ -32,7 +31,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Redirect = { play: async ({ canvasElement }) => { diff --git a/src/Authorization/SignedInWrapper.stories.tsx b/src/Authorization/SignedInWrapper.stories.tsx new file mode 100644 index 000000000..a83435bef --- /dev/null +++ b/src/Authorization/SignedInWrapper.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from '@storybook/test'; + +import { CompleteMember } from '@graasp/sdk'; + +import BuildIcon from '@/icons/BuildIcon.js'; + +import SignedInWrapper from './SignedInWrapper.js'; + +const redirectionLink = 'https://redirect.org'; + +const meta: Meta = { + title: 'Actions/SignedInWrapper', + component: SignedInWrapper, + + argTypes: { + onRedirect: { action: 'onRedirect' }, + }, + args: { + redirectionLink, + children: ( +
+ + + +
+ ), + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Authorized: Story = { + args: { + currentAccount: { id: 'member', name: 'member' } as CompleteMember, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // should see content + await expect(canvas.getByTestId('content')).toBeVisible(); + }, +}; diff --git a/src/Authorization/SignedInWrapper.tsx b/src/Authorization/SignedInWrapper.tsx new file mode 100644 index 000000000..a33f79e50 --- /dev/null +++ b/src/Authorization/SignedInWrapper.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; + +import { CurrentAccount, redirect } from '@graasp/sdk'; + +import RedirectionContent from './RedirectionContent.js'; + +export type SignedInWrapperProps = { + redirectionLink: string; + currentAccount?: CurrentAccount | null; + onRedirect?: () => void; + children?: ReactNode; +}; + +const SignedInWrapper = ({ + currentAccount, + redirectionLink, + onRedirect, + children, +}: SignedInWrapperProps): SignedInWrapperProps['children'] => { + const redirectToSignIn = (): void => { + if (!redirectionLink) { + console.debug('No link has been set for redirection'); + return; + } + redirect(window, redirectionLink); + }; + + // check authorization: user shouldn't be empty + if (currentAccount?.id) { + return children; + } + + onRedirect?.(); + + redirectToSignIn(); + + // redirect page if redirection is not working + return ; +}; + +export default SignedInWrapper; diff --git a/src/Authorization/withAuthorization.stories.tsx b/src/Authorization/withAuthorization.stories.tsx deleted file mode 100644 index 41dc1bdd7..000000000 --- a/src/Authorization/withAuthorization.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { CompleteMember } from '@graasp/sdk'; - -import BuildIcon from '../icons/BuildIcon.js'; -import withAuthorization from './withAuthorization.js'; - -const redirectionLink = 'http://redirect.org'; - -const ComponentWithAuthorization = withAuthorization(BuildIcon, { - redirectionLink, - currentMember: { id: 'member', name: 'member' } as CompleteMember, -}); - -const meta: Meta = { - title: 'Actions/Authorization', - component: ComponentWithAuthorization, - - argTypes: { - onRedirect: { action: 'onRedirect' }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Authorized: Story = { - render: () => { - const Component = withAuthorization(BuildIcon, { - redirectionLink, - currentMember: { id: 'member', name: 'member' } as CompleteMember, - }); - return ; - }, -}; diff --git a/src/Authorization/withAuthorization.tsx b/src/Authorization/withAuthorization.tsx deleted file mode 100644 index 77e87e654..000000000 --- a/src/Authorization/withAuthorization.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ComponentType } from 'react'; - -import { CompleteMember, redirect } from '@graasp/sdk'; - -import RedirectionContent from './RedirectionContent.js'; - -export type WithAuthorizationProps = { - redirectionLink: string; - currentMember?: CompleteMember | null; - onRedirect?: () => void; -}; - -const withAuthorization = -

( - ChildComponent: ComponentType

, - { currentMember, redirectionLink, onRedirect }: WithAuthorizationProps, - ): ((props: P) => JSX.Element) => - (childProps: P) => { - const redirectToSignIn = (): void => { - if (!redirectionLink) { - return console.debug('No link has been set for redirection'); - } - redirect(window, redirectionLink); - }; - - // check authorization: user shouldn't be empty - if (currentMember && currentMember.id) { - return ; - } - - onRedirect?.(); - - redirectToSignIn(); - - // redirect page if redirection is not working - return ; - }; - -export default withAuthorization; diff --git a/src/UserSwitch/UserSwitch.stories.tsx b/src/UserSwitch/UserSwitch.stories.tsx index 38f6697b9..a32e12794 100644 --- a/src/UserSwitch/UserSwitch.stories.tsx +++ b/src/UserSwitch/UserSwitch.stories.tsx @@ -20,7 +20,7 @@ type Story = StoryObj; export const SignedIn = { args: { currentMember: MOCK_CURRENT_MEMBER, - renderAvatar: () => ( + avatar: ( ( + avatar: ( MouseEventHandler; - renderAvatar?: (member?: CompleteMember | null) => JSX.Element; + avatar?: JSX.Element; signedOutTooltipText?: string; }; @@ -49,7 +49,7 @@ export const UserSwitch = ({ isMemberLoading = false, currentMember, menuId, - renderAvatar = () => <>, + avatar, signedOutTooltipText = 'You are not signed in.', }: Props): JSX.Element => { const [anchorEl, setAnchorEl] = useState<(EventTarget & Element) | null>( @@ -121,7 +121,7 @@ export const UserSwitch = ({ return ( - {renderAvatar(currentMember)} + {avatar}

@@ -130,7 +130,7 @@ export const UserSwitch = ({ {/* show info only for normal member */} {/* todo: show which item a pseudonymized member as access to */} - {!isPseudoMember(currentMember) && ( + {currentMember.type === AccountType.Individual && ( <> {currentMember.email} @@ -165,7 +165,7 @@ export const UserSwitch = ({ return ( <> - {renderAvatar(currentMember)} + {avatar} {memberName && !isMobile && ( = { +const meta = { title: 'Common/UserSwitch/UserSwitchWrapper', component: UserSwitchWrapper, -}; + argTypes: { + signOut: { action: 'signOut' }, + }, + args: { + signOut: async () => {}, + profilePath: 'profilePath', + redirectPath: 'redirectPath', + }, +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const SignedIn: Story = { +export const SignedIn = { args: { - currentMember: MOCK_CURRENT_MEMBER, seeProfileText: 'See Profile', signOutText: 'Sign Out', - renderAvatar: () => ( + currentMember: MOCK_CURRENT_MEMBER, + avatar: ( ), }, -}; -SignedIn.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // open dialog + const nameText = canvas.getByLabelText(MOCK_CURRENT_MEMBER.name); + await userEvent.click(nameText); + + const menuCanvas = within(await screen.getByRole('menu')); + + // profile button + const profileButton = menuCanvas.getByText(SignedIn.args!.seeProfileText!); + expect(profileButton).toBeInTheDocument(); + + // email + const emailText = menuCanvas.getByText(MOCK_CURRENT_MEMBER.email); + expect(emailText).toBeInTheDocument(); + + // sign out button + const signOutButton = menuCanvas.getByText(SignedIn.args!.signOutText!); + expect(signOutButton).toBeInTheDocument(); + }, +} satisfies Story; - // open dialog - const nameText = canvas.getByLabelText(MOCK_CURRENT_MEMBER.name); - await userEvent.click(nameText); +export const Guest = { + args: { + seeProfileText: 'See Profile', + signOutText: 'Sign Out', + currentMember: GuestFactory({ + itemLoginSchema: ItemLoginSchemaFactory({ + item: PackedFolderItemFactory(), + type: ItemLoginSchemaType.Username, + }), + }), + avatar: ( + + ), + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); - const menuCanvas = within(await screen.getByRole('menu')); + // open dialog + const nameText = canvas.getByLabelText(args.currentMember!.name); + await userEvent.click(nameText); - // profile button - const profileButton = menuCanvas.getByText(SignedIn.args!.seeProfileText!); - expect(profileButton).toBeInTheDocument(); + const menuCanvas = within(await screen.getByRole('menu')); - // email - const emailText = menuCanvas.getByText(MOCK_CURRENT_MEMBER.email); - expect(emailText).toBeInTheDocument(); + // have 2 menu items - do not show profile button + expect(menuCanvas.getAllByRole('menuitem')).toHaveLength(2); - // sign out button - const signOutButton = menuCanvas.getByText(SignedIn.args!.signOutText!); - expect(signOutButton).toBeInTheDocument(); -}; + // sign out button + const signOutButton = menuCanvas.getByText(SignedIn.args!.signOutText!); + expect(signOutButton).toBeInTheDocument(); + }, +} satisfies Story; -export const SignedOut: Story = { +export const SignedOut = { args: { switchMemberText: 'Sign In', - renderAvatar: () => ( + avatar: ( ), }, -}; - -SignedOut.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); - // open dialog - const nameText = canvas.getByRole('button'); - await userEvent.click(nameText); + // open dialog + const nameText = canvas.getByRole('button'); + await userEvent.click(nameText); - // custom content - const menuCanvas = within(await screen.getByRole('menu')); - const signInButton = menuCanvas.getByText(SignedOut.args!.switchMemberText!); - expect(signInButton).toBeInTheDocument(); -}; + // custom content + const menuCanvas = within(await screen.getByRole('menu')); + const signInButton = menuCanvas.getByText( + SignedOut.args!.switchMemberText!, + ); + expect(signInButton).toBeInTheDocument(); + }, +} satisfies Story; diff --git a/src/UserSwitch/UserSwitchWrapper.tsx b/src/UserSwitch/UserSwitchWrapper.tsx index 8a4dda1f9..ce91b9bbd 100644 --- a/src/UserSwitch/UserSwitchWrapper.tsx +++ b/src/UserSwitch/UserSwitchWrapper.tsx @@ -4,7 +4,7 @@ import { } from '@mui/icons-material'; import { ListItemIcon, MenuItem, Typography } from '@mui/material'; -import { CompleteMember, redirect } from '@graasp/sdk'; +import { AccountType, CurrentAccount, redirect } from '@graasp/sdk'; import Loader from '../Loader/Loader.js'; import { UserSwitch } from './UserSwitch.js'; @@ -19,13 +19,13 @@ interface Props { buildMemberMenuItemId?: (id: string) => string; ButtonContent?: JSX.Element; buttonId?: string; - currentMember?: CompleteMember | null; + currentMember?: CurrentAccount | null; // domain: string; - isCurrentMemberLoading: boolean; + isCurrentMemberLoading?: boolean; // isCurrentMemberSuccess: boolean; profilePath: string; redirectPath: string; - renderAvatar: (member?: CompleteMember | null) => JSX.Element; + avatar: JSX.Element; seeProfileButtonId?: string; seeProfileText?: string; signedOutTooltipText?: string; @@ -35,13 +35,13 @@ interface Props { * @param memberId Id of the user to sign out (current user) * @returns Promise of void */ - signOut: (memberId: string) => Promise; + signOut: () => Promise; signOutMenuItemId?: string; signOutText?: string; // switchMember: (args: { memberId: string; domain: string }) => Promise; switchMemberText?: string; - userMenuItems: UserMenuItem[]; + userMenuItems?: UserMenuItem[]; // useMembers: (ids: string[]) => UseQueryResult>; } @@ -52,11 +52,11 @@ export const UserSwitchWrapper = ({ buttonId, currentMember, // domain, - isCurrentMemberLoading, + isCurrentMemberLoading = false, // isCurrentMemberSuccess, profilePath, redirectPath, - renderAvatar, + avatar, seeProfileButtonId, seeProfileText = 'See Profile', signedOutTooltipText = 'You are not signed in.', @@ -95,7 +95,7 @@ export const UserSwitchWrapper = ({ const handleSignOut = async (): Promise => { if (currentMember) { - await signOut(currentMember.id); + await signOut(); } // on sign out success should redirect to sign in redirect(window, redirectPath); @@ -107,7 +107,7 @@ export const UserSwitchWrapper = ({ return redirect(window, redirectPath); }; - const goToSettings = (): void => { + const goToProfile = (): void => { redirect(window, profilePath); }; @@ -127,25 +127,32 @@ export const UserSwitchWrapper = ({ )); if (currentMember && currentMember.id) { - Actions = [ - - - - - {seeProfileText} - , - ...MenuItems, + Actions = + currentMember.type === AccountType.Individual + ? [ + + + + + {seeProfileText} + , + ] + : []; + + Actions.push(...MenuItems); + + Actions.push( {signOutText} , - ]; + ); } else { Actions = [ @@ -167,7 +174,7 @@ export const UserSwitchWrapper = ({ signedOutTooltipText={signedOutTooltipText} buttonId={buttonId} buildMemberMenuItemId={buildMemberMenuItemId} - renderAvatar={renderAvatar} + avatar={avatar} /> ); }; diff --git a/src/index.ts b/src/index.ts index 9b0acb788..bc80d5fc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,8 @@ export { default as CreativeCommons } from './CreativeCommons/CreativeCommons.js export { default as CookiesBanner } from './CookiesBanner/CookiesBanner.js'; -export { default as withAuthorization } from './Authorization/withAuthorization.js'; +export { default as SignedInWrapper } from './Authorization/SignedInWrapper.js'; +export { default as PreventGuestWrapper } from './Authorization/PreventGuestWrapper.js'; export { default as RedirectionContent } from './Authorization/RedirectionContent.js'; export { UserSwitch } from './UserSwitch/UserSwitch.js'; diff --git a/src/itemLogin/ItemLoginAuthorization.stories.tsx b/src/itemLogin/ItemLoginAuthorization.stories.tsx new file mode 100644 index 000000000..f44937251 --- /dev/null +++ b/src/itemLogin/ItemLoginAuthorization.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect } from '@storybook/test'; +import { within } from '@storybook/testing-library'; + +import { + CompleteMember, + ItemLoginSchemaType, + PackedDocumentItemFactory, +} from '@graasp/sdk'; + +import Card from '@/Card/Card.js'; + +import ItemLoginAuthorization from './ItemLoginAuthorization.js'; +import { FORBIDDEN_TEXT } from './constants.js'; + +const item = PackedDocumentItemFactory(); +const meta = { + title: 'Actions/ItemLoginAuthorization', + component: ItemLoginAuthorization, + + argTypes: { + signIn: { action: 'onRedirect' }, + }, + args: { + signIn: () => {}, + itemId: item.id, + + children: , + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Authorized = { + args: { + currentAccount: { id: 'member', name: 'member' } as CompleteMember, + item, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('card')).toBeVisible(); + }, +} satisfies Story; + +export const LogInForm = { + args: { + itemLoginSchemaType: ItemLoginSchemaType.Username, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText('Sign In')).toBeVisible(); + }, +} satisfies Story; + +export const Loading = { + args: { + isLoading: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByRole('progressbar')).toBeVisible(); + }, +} satisfies Story; + +export const Forbidden = { + args: { + currentAccount: { id: 'member', name: 'member' } as CompleteMember, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText(FORBIDDEN_TEXT)).toBeVisible(); + }, +} satisfies Story; diff --git a/src/itemLogin/ItemLoginAuthorization.tsx b/src/itemLogin/ItemLoginAuthorization.tsx index 9f7747f10..e3631b99b 100644 --- a/src/itemLogin/ItemLoginAuthorization.tsx +++ b/src/itemLogin/ItemLoginAuthorization.tsx @@ -1,12 +1,7 @@ -import type { UseQueryResult } from '@tanstack/react-query'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; - -import { Alert } from '@mui/material'; - -import { ReactElement } from 'react'; +import { ReactElement, ReactNode } from 'react'; import { - CompleteMember, + CurrentAccount, DiscriminatedItem, ItemLoginSchemaType, UUID, @@ -19,99 +14,58 @@ import ItemLoginScreen, { SignInPropertiesType } from './ItemLoginScreen.js'; export type ItemLoginAuthorizationProps = { signIn: (args: { itemId: string } & SignInPropertiesType) => void; itemId: UUID; - useCurrentMember: () => UseQueryResult; - useItem: (itemId?: string) => UseQueryResult; - useItemLoginSchemaType: (args: { - itemId?: string; - }) => UseQueryResult; - Error?: ReactElement; - memberIdInputId?: string; + currentAccount?: CurrentAccount | null; + item?: DiscriminatedItem; + itemLoginSchemaType?: ItemLoginSchemaType; usernameInputId?: string; signInButtonId?: string; passwordInputId?: string; + children?: ReactNode; ForbiddenContent?: ReactElement; + isLoading?: boolean; }; -const ItemLoginAuthorization = - ({ - useCurrentMember, - useItem, - useItemLoginSchemaType, - itemId, - signIn, - Error: ErrorComponent, - usernameInputId, - signInButtonId, - passwordInputId, - ForbiddenContent = , - }: ItemLoginAuthorizationProps) => - (ChildComponent: () => JSX.Element) => { - const ComposedComponent = (): ReactElement => { - const { - data: user, - isLoading: isMemberLoading, - isError: isCurrentMemberError, - } = useCurrentMember(); - const { data: itemLoginSchemaType } = useItemLoginSchemaType({ itemId }); - const { - data: item, - isLoading: isItemLoading, - error: itemError, - isError: isItemError, - } = useItem(itemId); - - if (isMemberLoading || (isItemLoading && !item)) { - // get item login if the user is not authenticated and the item is empty - return ; - } - - // member should never trigger an error - // but can be empty - if (isCurrentMemberError) { - return ( - ErrorComponent ?? An error occurred. - ); - } +const ItemLoginAuthorization = ({ + currentAccount, + item, + itemLoginSchemaType, + itemId, + signIn, + isLoading, + usernameInputId, + signInButtonId, + passwordInputId, + ForbiddenContent = , + children, +}: ItemLoginAuthorizationProps): ReactNode => { + if (isLoading) { + return ; + } - if ( - isItemError && - [ - getReasonPhrase(StatusCodes.BAD_REQUEST), - getReasonPhrase(StatusCodes.NOT_FOUND), - ].includes((itemError as Error).message) - ) { - return ( - ErrorComponent ?? An error occurred. - ); - } + // the item could be fetched without errors + // because the user is signed in and has access + // or because the item is public + if (item && item.id) { + return children; + } - // the item could be fetched without errors - // because the user is signed in and has access - // or because the item is public - if (item && item.id) { - return ; - } + // signed out but can sign in with item login + if (!currentAccount?.id && itemLoginSchemaType) { + return ( + + ); + } - // signed out but can sign in with item login - if ((!user || !user.id) && itemLoginSchemaType) { - return ( - - ); - } - - // either the item does not allow item login - // or the user is already signed in as normal user and hasn't the access to this item - return ForbiddenContent; - }; - - return ComposedComponent; - }; + // either the item does not allow item login + // or the user is already signed in as normal user and hasn't the access to this item + return ForbiddenContent; +}; export default ItemLoginAuthorization; diff --git a/src/itemLogin/ItemLoginScreen.stories.tsx b/src/itemLogin/ItemLoginScreen.stories.tsx index 85a1ead8a..4f95e17c8 100644 --- a/src/itemLogin/ItemLoginScreen.stories.tsx +++ b/src/itemLogin/ItemLoginScreen.stories.tsx @@ -2,18 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, fn } from '@storybook/test'; import { userEvent, within } from '@storybook/testing-library'; -import { ItemLoginSchemaType } from '@graasp/sdk'; +import { ItemLoginSchemaType, PackedDocumentItemFactory } from '@graasp/sdk'; import { TABLE_CATEGORIES } from '../utils/storybook.js'; import ItemLoginScreen from './ItemLoginScreen.js'; import { FORBIDDEN_TEXT } from './constants.js'; -const meta: Meta = { +const item = PackedDocumentItemFactory(); +const meta = { title: 'Actions/ItemLogin/ItemLoginScreen', component: ItemLoginScreen, args: { signIn: fn(), + itemId: item.id, }, argTypes: { passwordInputId: { @@ -33,11 +35,11 @@ const meta: Meta = { }, signIn: { action: 'signin' }, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const ItemLoginUsernameAndPassword: Story = { args: { @@ -55,7 +57,7 @@ export const ItemLoginUsernameAndPassword: Story = { expect(args.signIn).toHaveBeenCalled(); }, -}; +} satisfies Story; export const ItemLoginUsername: Story = { args: { @@ -72,13 +74,15 @@ export const ItemLoginUsername: Story = { expect(args.signIn).toHaveBeenCalled(); }, -}; +} satisfies Story; -export const Forbidden: Story = { - args: {}, +export const Forbidden = { + args: { + itemId: item.id, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getByText(FORBIDDEN_TEXT)).toBeInTheDocument(); }, -}; +} satisfies Story; diff --git a/src/itemLogin/ItemLoginScreen.tsx b/src/itemLogin/ItemLoginScreen.tsx index e2f21f003..03e8c2e2c 100644 --- a/src/itemLogin/ItemLoginScreen.tsx +++ b/src/itemLogin/ItemLoginScreen.tsx @@ -37,7 +37,7 @@ export type ItemLoginScreenProps = { /** * item login schema object */ - itemLoginSchemaType: ItemLoginSchemaType; + itemLoginSchemaType?: ItemLoginSchemaType; signIn: (args: { itemId: string } & SignInPropertiesType) => void; /** * content to display when the user doesn't have access