-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement what is needed to have a private dashboard with the a…
…bility to trigger synchronizations
- Loading branch information
Showing
25 changed files
with
1,079 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { Meta, StoryFn } from '@storybook/react'; | ||
import { within } from '@storybook/test'; | ||
|
||
import { userSessionContext } from '@ad/.storybook/auth'; | ||
import { StoryHelperFactory } from '@ad/.storybook/helpers'; | ||
import { playFindMain, playFindProgressBar } from '@ad/.storybook/testing'; | ||
import { PrivateLayout } from '@ad/src/app/(private)/PrivateLayout'; | ||
import { Loading as PublicLayoutLoadingStory, Lorem as PublicLayoutLoremStory } from '@ad/src/app/(public)/PublicLayout.stories'; | ||
import { collaboratorOfSample } from '@ad/src/fixtures/ui'; | ||
import { UserInterfaceSessionSchema, UserInterfaceSessionSchemaType } from '@ad/src/models/entities/ui'; | ||
import { getTRPCMock } from '@ad/src/server/mock/trpc'; | ||
import { linkRegistry } from '@ad/src/utils/routes/registry'; | ||
|
||
type ComponentType = typeof PrivateLayout; | ||
const { generateMetaDefault, prepareStory } = StoryHelperFactory<ComponentType>(); | ||
|
||
export default { | ||
title: 'Layouts/PrivatePages', | ||
component: PrivateLayout, | ||
excludeStories: ['interfaceSessionQueryFactory'], | ||
...generateMetaDefault({ | ||
parameters: { | ||
layout: 'fullscreen', | ||
msw: { | ||
handlers: [], | ||
}, | ||
}, | ||
}), | ||
} as Meta<ComponentType>; | ||
|
||
export function interfaceSessionQueryFactory(session: UserInterfaceSessionSchemaType) { | ||
return { | ||
msw: { | ||
handlers: [ | ||
getTRPCMock({ | ||
type: 'query', | ||
path: ['getInterfaceSession'], | ||
response: { | ||
session: UserInterfaceSessionSchema.parse(session), | ||
}, | ||
}), | ||
], | ||
}, | ||
}; | ||
} | ||
|
||
const Template: StoryFn<ComponentType> = (args) => { | ||
return <PrivateLayout {...args} />; | ||
}; | ||
|
||
const AsNewUserStory = Template.bind({}); | ||
AsNewUserStory.args = {}; | ||
AsNewUserStory.parameters = { | ||
nextAuthMock: { | ||
session: userSessionContext, | ||
}, | ||
nextjs: { | ||
navigation: { | ||
pathname: linkRegistry.get('organization', { | ||
organizationId: collaboratorOfSample[0].id, | ||
}), | ||
}, | ||
}, | ||
...interfaceSessionQueryFactory({ | ||
collaboratorOf: [], | ||
isAdmin: false, | ||
}), | ||
}; | ||
AsNewUserStory.play = async ({ canvasElement }) => { | ||
await playFindMain(canvasElement); | ||
}; | ||
|
||
export const AsNewUser = prepareStory(AsNewUserStory); | ||
|
||
const AsCollaboratorStory = Template.bind({}); | ||
AsCollaboratorStory.args = {}; | ||
AsCollaboratorStory.parameters = { | ||
...AsNewUserStory.parameters, | ||
...interfaceSessionQueryFactory({ | ||
collaboratorOf: collaboratorOfSample, | ||
isAdmin: false, | ||
}), | ||
}; | ||
AsCollaboratorStory.play = async ({ canvasElement }) => { | ||
await playFindMain(canvasElement); | ||
}; | ||
|
||
export const AsCollaborator = prepareStory(AsCollaboratorStory); | ||
|
||
const LoremStory = Template.bind({}); | ||
LoremStory.args = { | ||
...PublicLayoutLoremStory.args, | ||
}; | ||
LoremStory.parameters = { | ||
...AsNewUserStory.parameters, | ||
}; | ||
LoremStory.play = async ({ canvasElement }) => { | ||
await playFindMain(canvasElement); | ||
}; | ||
|
||
export const Lorem = prepareStory(LoremStory); | ||
Lorem.storyName = 'With lorem'; | ||
|
||
const LoadingStory = Template.bind({}); | ||
LoadingStory.args = { | ||
...PublicLayoutLoadingStory.args, | ||
}; | ||
LoadingStory.parameters = { | ||
...AsNewUserStory.parameters, | ||
}; | ||
LoadingStory.play = async ({ canvasElement }) => { | ||
await playFindProgressBar(canvasElement, /chargement/i); | ||
}; | ||
|
||
export const Loading = prepareStory(LoadingStory); | ||
Loading.storyName = 'With loader'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
'use client'; | ||
|
||
import { Footer } from '@codegouvfr/react-dsfr/Footer'; | ||
import { Header } from '@codegouvfr/react-dsfr/Header'; | ||
import { HeaderProps } from '@codegouvfr/react-dsfr/Header'; | ||
import { MainNavigationProps } from '@codegouvfr/react-dsfr/MainNavigation'; | ||
import { MenuProps } from '@codegouvfr/react-dsfr/MainNavigation/Menu'; | ||
import Badge from '@mui/material/Badge'; | ||
import Grid from '@mui/material/Grid'; | ||
import { useRouter } from 'next/navigation'; | ||
import { usePathname } from 'next/navigation'; | ||
import { PropsWithChildren, useEffect, useState } from 'react'; | ||
|
||
import { trpc } from '@ad/src/client/trpcClient'; | ||
import { ContentWrapper } from '@ad/src/components/ContentWrapper'; | ||
import { ErrorAlert } from '@ad/src/components/ErrorAlert'; | ||
import { FlashMessage } from '@ad/src/components/FlashMessage'; | ||
import { LoadingArea } from '@ad/src/components/LoadingArea'; | ||
import { useLiveChat } from '@ad/src/components/live-chat/useLiveChat'; | ||
import { UserInterfaceSessionProvider } from '@ad/src/components/user-interface-session/UserInterfaceSessionProvider'; | ||
import { signIn, useSession } from '@ad/src/proxies/next-auth/react'; | ||
import { commonFooterAttributes, commonHeaderAttributes, organizationSwichQuickAccessItem, userQuickAccessItem } from '@ad/src/utils/dsfr'; | ||
import { centeredAlertContainerGridProps } from '@ad/src/utils/grid'; | ||
import { linkRegistry } from '@ad/src/utils/routes/registry'; | ||
import { hasPathnameThisMatch } from '@ad/src/utils/url'; | ||
|
||
export function PrivateLayout(props: PropsWithChildren) { | ||
const router = useRouter(); | ||
const pathname = usePathname(); | ||
const sessionWrapper = useSession(); | ||
const [logoutCommitted, setLogoutCommitted] = useState(false); | ||
|
||
const { data, error, isLoading, refetch } = trpc.getInterfaceSession.useQuery({}); | ||
|
||
useEffect(() => { | ||
if (sessionWrapper.status === 'unauthenticated' && !logoutCommitted) { | ||
signIn(); | ||
} | ||
}, [logoutCommitted, router, sessionWrapper.status]); | ||
|
||
const { showLiveChat } = useLiveChat(); | ||
|
||
if (isLoading || sessionWrapper.status !== 'authenticated') { | ||
return <LoadingArea ariaLabelTarget="contenu" />; | ||
} else if (error) { | ||
return ( | ||
<Grid container {...centeredAlertContainerGridProps}> | ||
<ErrorAlert errors={[error]} refetchs={[refetch]} /> | ||
</Grid> | ||
); | ||
} | ||
|
||
const userInterfaceSession = data?.session; | ||
|
||
const currentOrganization = userInterfaceSession.collaboratorOf.find((organization) => { | ||
const organizationPageBaseUrl = linkRegistry.get('organization', { | ||
organizationId: organization.id, | ||
}); | ||
|
||
if (pathname?.startsWith(organizationPageBaseUrl)) { | ||
return true; | ||
} | ||
|
||
return false; | ||
}); | ||
|
||
const dashboardLink = linkRegistry.get('dashboard', undefined); | ||
|
||
const navigation: MainNavigationProps.Item[] = [ | ||
{ | ||
isActive: hasPathnameThisMatch(pathname, dashboardLink), | ||
text: 'Tableau de bord', | ||
linkProps: { | ||
href: dashboardLink, | ||
target: '_self', | ||
}, | ||
}, | ||
]; | ||
|
||
const quickAccessItems: HeaderProps.QuickAccessItem[] = [ | ||
{ | ||
iconId: 'fr-icon-questionnaire-line', | ||
buttonProps: { | ||
onClick: (event) => { | ||
showLiveChat(); | ||
}, | ||
}, | ||
text: 'Support', | ||
}, | ||
userQuickAccessItem(sessionWrapper.data?.user), | ||
]; | ||
|
||
if (userInterfaceSession.collaboratorOf.length) { | ||
quickAccessItems.unshift( | ||
organizationSwichQuickAccessItem({ | ||
organizations: userInterfaceSession.collaboratorOf, | ||
currentOrganization: currentOrganization, | ||
}) | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<UserInterfaceSessionProvider session={userInterfaceSession}> | ||
<Header {...commonHeaderAttributes} quickAccessItems={quickAccessItems} navigation={navigation} /> | ||
<FlashMessage appMode={process.env.NEXT_PUBLIC_APP_MODE} nodeEnv={process.env.NODE_ENV} /> | ||
<ContentWrapper>{props.children}</ContentWrapper> | ||
<Footer {...commonFooterAttributes} /> | ||
</UserInterfaceSessionProvider> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Meta, StoryFn } from '@storybook/react'; | ||
|
||
import { StoryHelperFactory } from '@ad/.storybook/helpers'; | ||
import { playFindMainTitle } from '@ad/.storybook/testing'; | ||
import { AsNewUser as PrivateLayoutAsNewUserStory, interfaceSessionQueryFactory } from '@ad/src/app/(private)/PrivateLayout.stories'; | ||
import { DashboardPage } from '@ad/src/app/(private)/dashboard/DashboardPage'; | ||
import { collaboratorOfSample } from '@ad/src/fixtures/ui'; | ||
|
||
type ComponentType = typeof DashboardPage; | ||
const { generateMetaDefault, prepareStory } = StoryHelperFactory<ComponentType>(); | ||
|
||
export default { | ||
title: 'Pages/Dashboard', | ||
component: DashboardPage, | ||
...generateMetaDefault({ | ||
parameters: {}, | ||
}), | ||
} as Meta<ComponentType>; | ||
|
||
const Template: StoryFn<ComponentType> = (args) => { | ||
return <DashboardPage {...args} />; | ||
}; | ||
|
||
const AsNewUserStory = Template.bind({}); | ||
AsNewUserStory.args = {}; | ||
AsNewUserStory.parameters = { | ||
...interfaceSessionQueryFactory({ | ||
collaboratorOf: [], | ||
isAdmin: false, | ||
}), | ||
}; | ||
AsNewUserStory.play = async ({ canvasElement }) => { | ||
await playFindMainTitle(canvasElement, /Bienvenue/i); | ||
}; | ||
|
||
export const AsNewUser = prepareStory(AsNewUserStory); | ||
|
||
const AsNewUserWithLayoutStory = Template.bind({}); | ||
AsNewUserWithLayoutStory.args = {}; | ||
AsNewUserWithLayoutStory.parameters = { | ||
layout: 'fullscreen', | ||
...AsNewUserStory.parameters, | ||
}; | ||
AsNewUserWithLayoutStory.play = async ({ canvasElement }) => { | ||
await playFindMainTitle(canvasElement, /Bienvenue/i); | ||
}; | ||
|
||
export const AsNewUserWithLayout = prepareStory(AsNewUserWithLayoutStory, { | ||
layoutStory: PrivateLayoutAsNewUserStory, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
'use client'; | ||
|
||
import Button from '@mui/lab/LoadingButton'; | ||
import Container from '@mui/material/Container'; | ||
import Grid from '@mui/material/Grid'; | ||
import Typography from '@mui/material/Typography'; | ||
import { redirect } from 'next/navigation'; | ||
|
||
import { trpc } from '@ad/src/client/trpcClient'; | ||
import { ErrorAlert } from '@ad/src/components/ErrorAlert'; | ||
import { LoadingArea } from '@ad/src/components/LoadingArea'; | ||
import { useLiveChat } from '@ad/src/components/live-chat/useLiveChat'; | ||
import { centeredAlertContainerGridProps } from '@ad/src/utils/grid'; | ||
import { linkRegistry } from '@ad/src/utils/routes/registry'; | ||
|
||
export interface DashboardPageProps {} | ||
|
||
export function DashboardPage(props: DashboardPageProps) { | ||
const { showLiveChat, isLiveChatLoading } = useLiveChat(); | ||
|
||
const { data, error, isLoading, refetch } = trpc.getInterfaceSession.useQuery({}); | ||
|
||
if (isLoading) { | ||
return <LoadingArea ariaLabelTarget="contenu" />; | ||
} else if (error) { | ||
return ( | ||
<Grid container {...centeredAlertContainerGridProps}> | ||
<ErrorAlert errors={[error]} refetchs={[refetch]} /> | ||
</Grid> | ||
); | ||
} | ||
|
||
const userInterfaceSession = data.session; | ||
|
||
// Since we have a manual onboarding for now we can consider all users having at last 1 organization | ||
if (userInterfaceSession.collaboratorOf.length) { | ||
const organization = userInterfaceSession.collaboratorOf[0]; | ||
|
||
redirect( | ||
linkRegistry.get('organization', { | ||
organizationId: organization.id, | ||
}) | ||
); | ||
|
||
return; | ||
} else { | ||
// Simple user cannot see much | ||
return ( | ||
<Container | ||
sx={{ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
py: 3, | ||
}} | ||
> | ||
<Grid container sx={{ justifyContent: 'center' }}> | ||
<Typography component="h1" variant="h4" sx={{ textAlign: 'center', py: 2 }}> | ||
Bienvenue sur la plateforme ! | ||
</Typography> | ||
<Typography component="p" variant="body2" sx={{ textAlign: 'center', py: 2 }}> | ||
Vous n'avez actuellement aucun accès spécifique à la plateforme. Veuillez nous contacter par la messagerie. | ||
</Typography> | ||
<Button onClick={showLiveChat} loading={isLiveChatLoading} size="large" variant="contained"> | ||
Ouvrir la messagerie | ||
</Button> | ||
</Grid> | ||
</Container> | ||
); | ||
} | ||
} |
Oops, something went wrong.