♻️(frontend) create a doc from a modal

We refacto the create doc feature to use a modal
instead of a page and a card component.
It is more consistent with the other features.
This commit is contained in:
Anthony LC
2024-07-08 13:35:23 +02:00
committed by Anthony LC
parent 8007c45a35
commit de922e1c04
9 changed files with 214 additions and 244 deletions

View File

@@ -15,6 +15,7 @@ and this project adheres to
## Changed
- ♻️(frontend) replace docs panel with docs grid #120
- ♻️(frontend) create a doc from a modal #132
## [1.0.0] - 2024-07-02

View File

@@ -14,18 +14,12 @@ test.describe('Doc Create', () => {
await buttonCreateHomepage.click();
await expect(buttonCreateHomepage).toBeHidden();
const card = page.getByLabel('Create new document card').first();
await expect(card.getByLabel('Document name')).toBeVisible();
await expect(card.getByLabel('icon group')).toBeVisible();
const card = page.getByRole('dialog').first();
await expect(
card.getByRole('heading', {
name: 'Name the document',
level: 3,
}),
card.locator('h2').getByText('Create a new document'),
).toBeVisible();
await expect(card.getByLabel('Document name')).toBeVisible();
await expect(card.getByText('Is it public ?')).toBeVisible();
@@ -35,13 +29,7 @@ test.describe('Doc Create', () => {
}),
).toBeVisible();
await expect(
card.getByRole('button', {
name: 'Cancel',
}),
).toBeVisible();
await expect(page).toHaveURL('/docs/create/');
await expect(card.getByLabel('Close the modal')).toBeVisible();
});
test('checks the cancel button interaction', async ({ page }) => {
@@ -51,13 +39,9 @@ test.describe('Doc Create', () => {
await buttonCreateHomepage.click();
await expect(buttonCreateHomepage).toBeHidden();
const card = page.getByLabel('Create new document card').first();
const card = page.getByRole('dialog').first();
await card
.getByRole('button', {
name: 'Cancel',
})
.click();
await card.getByLabel('Close the modal').click();
await expect(buttonCreateHomepage).toBeVisible();
});

View File

@@ -6,7 +6,7 @@ import { Doc } from '../types';
import { KEY_LIST_DOC } from './useDocs';
type CreateDocParam = Pick<Doc, 'title' | 'is_public'>;
export type CreateDocParam = Pick<Doc, 'title' | 'is_public'>;
export const createDoc = async ({
title,

View File

@@ -1,75 +0,0 @@
import { Button, Switch } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import IconGroup from '@/assets/icons/icon-group2.svg';
import { Box, Card, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useCreateDoc } from '../api/useCreateDoc';
import { InputDocName } from './InputDocName';
export const CardCreateDoc = () => {
const { t } = useTranslation();
const router = useRouter();
const {
mutate: createDoc,
isError,
isPending,
error,
} = useCreateDoc({
onSuccess: (doc) => {
router.push(`/docs/${doc.id}`);
},
});
const [docName, setDocName] = useState('');
const [docPublic, setDocPublic] = useState(false);
const { colorsTokens } = useCunninghamTheme();
return (
<Card
$padding="big"
$height="70%"
$justify="space-between"
$width="100%"
$maxWidth="24rem"
$minWidth="22rem"
aria-label={t('Create new document card')}
>
<Box $gap="1rem">
<Box $align="center">
<IconGroup
width={44}
color={colorsTokens()['primary-text']}
aria-label={t('icon group')}
/>
<Text as="h3" $textAlign="center">
{t('Name the document')}
</Text>
</Box>
<InputDocName
label={t('Document name')}
{...{ error, isError, isPending, setDocName }}
/>
<Switch
label={t('Is it public ?')}
labelSide="right"
onChange={() => setDocPublic(!docPublic)}
/>
</Box>
<Box $justify="space-between" $direction="row" $align="center">
<StyledLink href="/">
<Button color="secondary">{t('Cancel')}</Button>
</StyledLink>
<Button
onClick={() => createDoc({ title: docName, is_public: docPublic })}
disabled={!docName}
>
{t('Create the document')}
</Button>
</Box>
</Card>
);
};

View File

@@ -0,0 +1,190 @@
import {
Alert,
Button,
Modal,
ModalSize,
Switch,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { UseMutationResult } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import { Box, Text } from '@/components';
import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
import { KEY_DOC, KEY_LIST_DOC } from '../api';
import { useCreateDoc } from '../api/useCreateDoc';
import { useUpdateDoc } from '../api/useUpdateDoc';
import IconEdit from '../assets/icon-edit.svg';
import { Doc } from '../types';
import { InputDocName } from './InputDocName';
interface ModalCreateDocProps {
onClose: () => void;
}
export const ModalCreateDoc = ({ onClose }: ModalCreateDocProps) => {
const router = useRouter();
const api = useCreateDoc({
onSuccess: (doc) => {
router.push(`/docs/${doc.id}`);
},
});
const { t } = useTranslation();
return (
<ModalDoc
{...{
buttonText: t('Create the document'),
onClose,
isPublic: false,
titleModal: t('Create a new document'),
validate: (title, is_public) =>
api.mutate({
is_public,
title,
}),
...api,
}}
/>
);
};
interface ModalUpdateDocProps {
onClose: () => void;
doc: Doc;
}
export const ModalUpdateDoc = ({ onClose, doc }: ModalUpdateDocProps) => {
const { toast } = useToastProvider();
const { t } = useTranslation();
const api = useUpdateDoc({
onSuccess: () => {
toast(t('The document has been updated.'), VariantType.SUCCESS, {
duration: 4000,
});
onClose();
},
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
});
return (
<ModalDoc
{...{
buttonText: t('Validate the modification'),
onClose,
initialTitle: doc.title,
isPublic: doc.is_public,
infoText: t('Enter the new name of the selected document.'),
titleModal: t('Update document "{{documentTitle}}"', {
documentTitle: doc.title,
}),
validate: (title, is_public) =>
api.mutate({
is_public,
title,
id: doc.id,
}),
...api,
}}
/>
);
};
type ModalDoc<T> = {
buttonText: string;
isPublic: boolean;
onClose: () => void;
titleModal: string;
validate: (title: string, is_public: boolean) => void;
initialTitle?: string;
infoText?: string;
} & UseMutationResult<Doc, APIError<unknown>, T, unknown>;
const ModalDoc = <T,>({
buttonText,
infoText,
initialTitle,
isPublic,
onClose,
titleModal,
validate,
...api
}: ModalDoc<T>) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const [title, setTitle] = useState(initialTitle || '');
const [docPublic, setDocPublic] = useState(isPublic);
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
leftActions={
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
rightActions={
<Button
aria-label={buttonText}
color="primary"
fullWidth
onClick={() => validate(title, docPublic)}
>
{buttonText}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem" $margin={{ bottom: '2.5rem' }}>
<IconEdit width={48} color={colorsTokens()['primary-text']} />
<Text as="h2" $size="h3" $margin="none">
{titleModal}
</Text>
</Box>
}
>
<Box $margin={{ bottom: 'xl' }} $gap="1rem">
{infoText && (
<Alert canClose={false} type={VariantType.INFO}>
<Text>{infoText}</Text>
</Alert>
)}
<Box $gap="1rem">
<InputDocName
label={t('Document name')}
defaultValue={title}
{...{
error: api.error,
isError: api.isError,
isPending: api.isPending,
setDocName: setTitle,
}}
/>
<Switch
label={t('Is it public ?')}
labelSide="right"
defaultChecked={docPublic}
onChange={() => setDocPublic(!docPublic)}
/>
</Box>
</Box>
</Modal>
);
};

View File

@@ -1,119 +0,0 @@
import {
Alert,
Button,
Modal,
ModalSize,
Switch,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
import { KEY_DOC, KEY_LIST_DOC } from '../api';
import { useUpdateDoc } from '../api/useUpdateDoc';
import IconEdit from '../assets/icon-edit.svg';
import { Doc } from '../types';
import { InputDocName } from './InputDocName';
interface ModalUpdateDocProps {
onClose: () => void;
doc: Doc;
}
export const ModalUpdateDoc = ({ onClose, doc }: ModalUpdateDocProps) => {
const { colorsTokens } = useCunninghamTheme();
const [title, setTitle] = useState(doc.title);
const { toast } = useToastProvider();
const [docPublic, setDocPublic] = useState(doc.is_public);
const { t } = useTranslation();
const {
mutate: updateDoc,
isError,
isPending,
error,
} = useUpdateDoc({
onSuccess: () => {
toast(t('The document has been updated.'), VariantType.SUCCESS, {
duration: 4000,
});
onClose();
},
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
});
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
leftActions={
<Button
aria-label={t('Close the modal')}
color="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
rightActions={
<Button
aria-label={t('Validate the modification')}
color="primary"
fullWidth
onClick={() =>
updateDoc({
title,
id: doc.id,
is_public: docPublic,
})
}
>
{t('Validate the modification')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconEdit width={48} color={colorsTokens()['primary-text']} />
<Text as="h2" $size="h3" $margin="none">
{t('Update document "{{documentTitle}}"', {
documentTitle: doc.title,
})}
</Text>
</Box>
}
>
<Box
$margin={{ bottom: 'xl' }}
aria-label={t('Content modal to update the document')}
$gap="1rem"
>
<Alert canClose={false} type={VariantType.INFO}>
<Text>{t('Enter the new name of the selected document.')}</Text>
</Alert>
<Box $gap="1rem">
<InputDocName
label={t('Document name')}
defaultValue={title}
{...{ error, isError, isPending, setDocName: setTitle }}
/>
<Switch
label={t('Is it public ?')}
labelSide="right"
defaultChecked={docPublic}
onChange={() => setDocPublic(!docPublic)}
/>
</Box>
</Box>
</Modal>
);
};

View File

@@ -1,3 +1,2 @@
export * from './CardCreateDoc';
export * from './ModalRemoveDoc';
export * from './ModalUpdateDoc';
export * from './ModalCreateUpdateDoc';

View File

@@ -1,23 +1,33 @@
import { Button } from '@openfun/cunningham-react';
import React from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, StyledLink } from '@/components';
import { Box, Card } from '@/components';
import { ModalCreateDoc } from '@/features/docs/doc-management';
import { DocsGrid } from './DocsGrid';
export const DocsGridContainer = () => {
const { t } = useTranslation();
const [isModalCreateOpen, setIsModalCreateOpen] = useState(false);
return (
<Box $overflow="auto">
<Card $margin="big" $padding="tiny">
<Box $align="flex-end" $justify="center">
<StyledLink href="/docs/create">
<Button>{t('Create a new document')}</Button>
</StyledLink>
<Button
onClick={() => {
setIsModalCreateOpen(true);
}}
>
{t('Create a new document')}
</Button>
</Box>
</Card>
<DocsGrid />
{isModalCreateOpen && (
<ModalCreateDoc onClose={() => setIsModalCreateOpen(false)} />
)}
</Box>
);
};

View File

@@ -1,20 +0,0 @@
import { ReactElement } from 'react';
import { Box } from '@/components';
import { CardCreateDoc } from '@/features/docs/doc-management';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return (
<Box $padding="large" $justify="center" $align="start" $height="inherit">
<CardCreateDoc />
</Box>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
};
export default Page;