Skip to content

Commit

Permalink
feat: implement what is needed to have a private dashboard with the a…
Browse files Browse the repository at this point in the history
…bility to trigger synchronizations
  • Loading branch information
sneko committed Dec 19, 2024
1 parent b2f3b58 commit ae6db76
Show file tree
Hide file tree
Showing 25 changed files with 1,079 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .storybook/testing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export async function playFindMainTitle(parentElement: HTMLElement, name: string
});
}

export async function playFindButton(parentElement: HTMLElement, name: string | RegExp): Promise<HTMLElement> {
return await within(parentElement).findByRole('button', {
name: name,
});
}

export async function playFindProgressBar(parentElement: HTMLElement, name: string | RegExp): Promise<HTMLElement> {
return await within(parentElement).findByRole('progressbar', {
name: name,
Expand Down
116 changes: 116 additions & 0 deletions src/app/(private)/PrivateLayout.stories.tsx
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';
112 changes: 112 additions & 0 deletions src/app/(private)/PrivateLayout.tsx
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>
</>
);
}
50 changes: 50 additions & 0 deletions src/app/(private)/dashboard/DashboardPage.stories.tsx
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,
});
71 changes: 71 additions & 0 deletions src/app/(private)/dashboard/DashboardPage.tsx
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&apos;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>
);
}
}
Loading

0 comments on commit ae6db76

Please sign in to comment.