♻️(frontend) improve the ui error and message info ui

Improve the ui error and message info ui:
- Can use a icon in TextErrors component
- use mode the Alert component to display message info
This commit is contained in:
Anthony LC
2024-06-14 11:39:57 +02:00
committed by Anthony LC
parent 62098ec753
commit 5ba35dbc1d
12 changed files with 146 additions and 119 deletions

View File

@@ -29,6 +29,7 @@ and this project adheres to
- (frontend) pdf has title doc (#84) - (frontend) pdf has title doc (#84)
- ⚡️(e2e) unique login between tests (#80) - ⚡️(e2e) unique login between tests (#80)
- ⚡️(CI) improve e2e job (#86) - ⚡️(CI) improve e2e job (#86)
- ♻️(frontend) improve the error and message info ui (#93)
## Fixed ## Fixed

View File

@@ -1,3 +1,5 @@
import { Alert, VariantType } from '@openfun/cunningham-react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Text, TextType } from '@/components'; import { Box, Text, TextType } from '@/components';
@@ -5,40 +7,38 @@ import { Box, Text, TextType } from '@/components';
interface TextErrorsProps extends TextType { interface TextErrorsProps extends TextType {
causes?: string[]; causes?: string[];
defaultMessage?: string; defaultMessage?: string;
icon?: ReactNode;
} }
export const TextErrors = ({ export const TextErrors = ({
causes, causes,
defaultMessage, defaultMessage,
icon,
...textProps ...textProps
}: TextErrorsProps) => { }: TextErrorsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Box> <Alert canClose={false} type={VariantType.ERROR} icon={icon}>
{causes && <Box $direction="column" $gap="0.2rem">
causes.map((cause, i) => ( {causes &&
<Text causes.map((cause, i) => (
key={`causes-${i}`} <Text
$margin={{ top: 'small' }} key={`causes-${i}`}
$theme="danger" $theme="danger"
$textAlign="center" $textAlign="center"
{...textProps} {...textProps}
> >
{cause} {cause}
</Text> </Text>
))} ))}
{!causes && ( {!causes && (
<Text <Text $theme="danger" $textAlign="center" {...textProps}>
$margin={{ top: 'small' }} {defaultMessage || t('Something bad happens, please retry.')}
$theme="danger" </Text>
$textAlign="center" )}
{...textProps} </Box>
> </Alert>
{defaultMessage || t('Something bad happens, please retry.')}
</Text>
)}
</Box>
); );
}; };

View File

@@ -172,13 +172,9 @@ describe('ModalRole', () => {
{ wrapper: AppWrapper }, { wrapper: AppWrapper },
); );
expect(
screen.getByText('You are the sole owner of this group.'),
).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
'Make another member the group owner, before you can change your own role.', 'You are the sole owner of this group, make another member the group owner, before you can change your own role.',
), ),
).toBeInTheDocument(); ).toBeInTheDocument();

View File

@@ -53,6 +53,7 @@ export const MemberAction = ({
}} }}
color="primary-text" color="primary-text"
icon={<span className="material-icons">edit</span>} icon={<span className="material-icons">edit</span>}
size="small"
> >
<Text $theme="primary">{t('Update role')}</Text> <Text $theme="primary">{t('Update role')}</Text>
</Button> </Button>
@@ -64,6 +65,7 @@ export const MemberAction = ({
}} }}
color="primary-text" color="primary-text"
icon={<span className="material-icons">delete</span>} icon={<span className="material-icons">delete</span>}
size="small"
> >
{t('Remove from group')} {t('Remove from group')}
</Button> </Button>

View File

@@ -1,12 +1,13 @@
import { import {
Alert,
Button, Button,
Modal, Modal,
ModalSize, ModalSize,
VariantType, VariantType,
useToastProvider, useToastProvider,
} from '@openfun/cunningham-react'; } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import IconUser from '@/assets/icons/icon-user.svg'; import IconUser from '@/assets/icons/icon-user.svg';
import { Box, Text, TextErrors } from '@/components'; import { Box, Text, TextErrors } from '@/components';
@@ -28,6 +29,7 @@ export const ModalDelete = ({ access, onClose, doc }: ModalDeleteProps) => {
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access); const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access);
const isNotAllowed = isOtherOwner || isLastOwner; const isNotAllowed = isOtherOwner || isLastOwner;
@@ -89,33 +91,28 @@ export const ModalDelete = ({ access, onClose, doc }: ModalDeleteProps) => {
} }
> >
<Box aria-label={t('Radio buttons to update the roles')}> <Box aria-label={t('Radio buttons to update the roles')}>
<Text> {!isLastOwner && !isOtherOwner && !isErrorUpdate && (
{t('Are you sure you want to remove this member from the document?')} <Alert canClose={false} type={VariantType.INFO}>
</Text> <Text>
{t(
{isErrorUpdate && ( 'Are you sure you want to remove this member from the document?',
<TextErrors )}
$margin={{ bottom: 'small' }} </Text>
causes={errorUpdate.cause} </Alert>
/>
)} )}
{(isLastOwner || isOtherOwner) && ( {isErrorUpdate && <TextErrors causes={errorUpdate.cause} />}
<Text
$theme="warning" {(isLastOwner || isOtherOwner) && !isErrorUpdate && (
$direction="row" <Alert canClose={false} type={VariantType.WARNING}>
$align="center" <Text>
$gap="0.5rem" {isLastOwner &&
$margin="tiny" t(
$justify="center" 'You are the last owner, you cannot be removed from your document.',
> )}
<span className="material-icons">warning</span> {isOtherOwner && t('You cannot remove other owner.')}
{isLastOwner && </Text>
t( </Alert>
'You are the last owner, you cannot be removed from your document.',
)}
{isOtherOwner && t('You cannot remove other owner.')}
</Text>
)} )}
<Text <Text

View File

@@ -1,4 +1,5 @@
import { import {
Alert,
Button, Button,
Modal, Modal,
ModalSize, ModalSize,
@@ -84,38 +85,36 @@ export const ModalRole = ({
title={t('Update the role')} title={t('Update the role')}
> >
<Box aria-label={t('Radio buttons to update the roles')}> <Box aria-label={t('Radio buttons to update the roles')}>
{isErrorUpdate && ( {isErrorUpdate && <TextErrors causes={errorUpdate.cause} />}
<TextErrors
$margin={{ bottom: 'small' }}
causes={errorUpdate.cause}
/>
)}
{(isLastOwner || isOtherOwner) && ( {(isLastOwner || isOtherOwner) && (
<Text <Box
$theme="warning"
$direction="row" $direction="row"
$align="center" $align="center"
$gap="0.5rem" $gap="0.5rem"
$margin={{ bottom: 'tiny', top: 'none' }} $margin={{ bottom: 'tiny', top: 'none' }}
as="div"
> >
<span className="material-icons">warning</span> <Alert
{isLastOwner && ( canClose={false}
<Box $align="flex-start"> type={VariantType.WARNING}
<Text $theme="warning"> icon={
{t('You are the sole owner of this group.')} <Text className="material-icons" $theme="warning">
warning
</Text> </Text>
<Text $theme="warning"> }
{t( >
'Make another member the group owner, before you can change your own role.', {isLastOwner && (
)} <Box $direction="column" $gap="0.2rem">
</Text> <Text $theme="warning">
</Box> {t(
)} 'You are the sole owner of this group, make another member the group owner, before you can change your own role.',
)}
{isOtherOwner && t('You cannot update the role of other owner.')} </Text>
</Text> </Box>
)}
{isOtherOwner && t('You cannot update the role of other owner.')}
</Alert>
</Box>
)} )}
<ChooseRole <ChooseRole

View File

@@ -1,4 +1,5 @@
import { import {
Alert,
Button, Button,
Modal, Modal,
ModalSize, ModalSize,
@@ -83,16 +84,18 @@ export const ModalRemovePad = ({ onClose, pad }: ModalRemovePadProps) => {
$margin={{ bottom: 'xl' }} $margin={{ bottom: 'xl' }}
aria-label={t('Content modal to delete document')} aria-label={t('Content modal to delete document')}
> >
<Text as="p" $margin={{ bottom: 'big' }}> {!isError && (
{t('Are you sure you want to delete the document "{{title}}"?', { <Alert canClose={false} type={VariantType.WARNING}>
title: pad.title, <Text>
})} {t('Are you sure you want to delete the document "{{title}}"?', {
</Text> title: pad.title,
})}
{isError && ( </Text>
<TextErrors $margin={{ bottom: 'small' }} causes={error.cause} /> </Alert>
)} )}
{isError && <TextErrors causes={error.cause} />}
<Text <Text
as="p" as="p"
$padding="small" $padding="small"

View File

@@ -1,4 +1,5 @@
import { import {
Alert,
Button, Button,
Modal, Modal,
ModalSize, ModalSize,
@@ -6,8 +7,8 @@ import {
VariantType, VariantType,
useToastProvider, useToastProvider,
} from '@openfun/cunningham-react'; } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components'; import { Box, Text } from '@/components';
import useCunninghamTheme from '@/cunningham/useCunninghamTheme'; import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
@@ -29,6 +30,7 @@ export const ModalUpdatePad = ({ onClose, pad }: ModalUpdatePadProps) => {
const [title, setTitle] = useState(pad.title); const [title, setTitle] = useState(pad.title);
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const [padPublic, setPadPublic] = useState(pad.is_public); const [padPublic, setPadPublic] = useState(pad.is_public);
const { t } = useTranslation();
const { const {
mutate: updatePad, mutate: updatePad,
@@ -92,10 +94,11 @@ export const ModalUpdatePad = ({ onClose, pad }: ModalUpdatePadProps) => {
<Box <Box
$margin={{ bottom: 'xl' }} $margin={{ bottom: 'xl' }}
aria-label={t('Content modal to update the document')} aria-label={t('Content modal to update the document')}
$gap="1rem"
> >
<Text as="p" $margin={{ bottom: 'big' }}> <Alert canClose={false} type={VariantType.INFO}>
{t('Enter the new name of the selected document.')} <Text>{t('Enter the new name of the selected document.')}</Text>
</Text> </Alert>
<Box $gap="1rem"> <Box $gap="1rem">
<InputPadName <InputPadName

View File

@@ -1,4 +1,5 @@
import { import {
Alert,
Button, Button,
Loader, Loader,
Modal, Modal,
@@ -136,12 +137,15 @@ export const ModalPDF = ({ onClose, templateOptions, pad }: ModalPDFProps) => {
<Box <Box
$margin={{ bottom: 'xl' }} $margin={{ bottom: 'xl' }}
aria-label={t('Content modal to generate a PDF')} aria-label={t('Content modal to generate a PDF')}
$gap="1.5rem"
> >
<Text as="p" $margin={{ bottom: 'big' }}> <Alert canClose={false} type={VariantType.INFO}>
{t( <Text>
'Generate a PDF from your document, it will be inserted in the selected template.', {t(
)} 'Generate a PDF from your document, it will be inserted in the selected template.',
</Text> )}
</Text>
</Alert>
<Select <Select
clearable={false} clearable={false}

View File

@@ -120,9 +120,7 @@ describe('PanelPads', () => {
expect(screen.getByRole('status')).toBeInTheDocument(); expect(screen.getByRole('status')).toBeInTheDocument();
expect( expect(
await screen.findByText( await screen.findByText('Something bad happens, please retry.'),
'Something bad happens, please refresh the page.',
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });

View File

@@ -2,7 +2,8 @@ import { Loader } from '@openfun/cunningham-react';
import React, { useMemo, useRef } from 'react'; import React, { useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components'; import { APIError } from '@/api';
import { Box, Text, TextErrors } from '@/components';
import { InfiniteScroll } from '@/components/InfiniteScroll'; import { InfiniteScroll } from '@/components/InfiniteScroll';
import { Pad, usePads } from '@/features/pads/pad-management'; import { Pad, usePads } from '@/features/pads/pad-management';
@@ -12,23 +13,13 @@ import { PadItem } from './PadItem';
interface PanelTeamsStateProps { interface PanelTeamsStateProps {
isLoading: boolean; isLoading: boolean;
isError: boolean; error: APIError<unknown> | null;
pads?: Pad[]; pads?: Pad[];
} }
const PadListState = ({ isLoading, isError, pads }: PanelTeamsStateProps) => { const PadListState = ({ isLoading, error, pads }: PanelTeamsStateProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (isError) {
return (
<Box $justify="center" $margin={{ bottom: 'big' }}>
<Text $theme="danger" $align="center" $textAlign="center">
{t('Something bad happens, please refresh the page.')}
</Text>
</Box>
);
}
if (isLoading) { if (isLoading) {
return ( return (
<Box $align="center" $margin="large"> <Box $align="center" $margin="large">
@@ -37,7 +28,7 @@ const PadListState = ({ isLoading, isError, pads }: PanelTeamsStateProps) => {
); );
} }
if (!pads?.length) { if (!pads?.length && !error) {
return ( return (
<Box $justify="center" $margin="small"> <Box $justify="center" $margin="small">
<Text <Text
@@ -57,14 +48,35 @@ const PadListState = ({ isLoading, isError, pads }: PanelTeamsStateProps) => {
); );
} }
return pads.map((pad) => <PadItem pad={pad} key={pad.id} />); return (
<>
{pads?.map((pad) => <PadItem pad={pad} key={pad.id} />)}
{error && (
<Box
$justify="center"
$margin={{ vertical: 'big', horizontal: 'auto' }}
>
<TextErrors
causes={error.cause}
icon={
error.status === 502 ? (
<Text className="material-icons" $theme="danger">
wifi_off
</Text>
) : undefined
}
/>
</Box>
)}
</>
);
}; };
export const PadList = () => { export const PadList = () => {
const ordering = usePadPanelStore((state) => state.ordering); const ordering = usePadPanelStore((state) => state.ordering);
const { const {
data, data,
isError, error,
isLoading, isLoading,
fetchNextPage, fetchNextPage,
hasNextPage, hasNextPage,
@@ -93,7 +105,7 @@ export const PadList = () => {
$margin={{ top: 'none' }} $margin={{ top: 'none' }}
role="listbox" role="listbox"
> >
<PadListState isLoading={isLoading} isError={isError} pads={pads} /> <PadListState isLoading={isLoading} error={error} pads={pads} />
</InfiniteScroll> </InfiniteScroll>
</Box> </Box>
); );

View File

@@ -3,8 +3,7 @@ import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Box } from '@/components'; import { Box, Text, TextErrors } from '@/components/';
import { TextErrors } from '@/components/TextErrors';
import { PadEditor } from '@/features/pads/pad-editor'; import { PadEditor } from '@/features/pads/pad-editor';
import { usePad } from '@/features/pads/pad-management'; import { usePad } from '@/features/pads/pad-management';
import { PadLayout } from '@/layouts'; import { PadLayout } from '@/layouts';
@@ -36,7 +35,20 @@ const Pad = ({ id }: PadProps) => {
return null; return null;
} }
return <TextErrors causes={error.cause} />; return (
<Box $margin="large">
<TextErrors
causes={error.cause}
icon={
error.status === 502 ? (
<Text className="material-icons" $theme="danger">
wifi_off
</Text>
) : undefined
}
/>
</Box>
);
} }
if (isLoading || !pad) { if (isLoading || !pad) {