♻️(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:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './CardCreateDoc';
|
||||
export * from './ModalRemoveDoc';
|
||||
export * from './ModalUpdateDoc';
|
||||
export * from './ModalCreateUpdateDoc';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user