♻️(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
|
## Changed
|
||||||
|
|
||||||
- ♻️(frontend) replace docs panel with docs grid #120
|
- ♻️(frontend) replace docs panel with docs grid #120
|
||||||
|
- ♻️(frontend) create a doc from a modal #132
|
||||||
|
|
||||||
## [1.0.0] - 2024-07-02
|
## [1.0.0] - 2024-07-02
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,12 @@ test.describe('Doc Create', () => {
|
|||||||
await buttonCreateHomepage.click();
|
await buttonCreateHomepage.click();
|
||||||
await expect(buttonCreateHomepage).toBeHidden();
|
await expect(buttonCreateHomepage).toBeHidden();
|
||||||
|
|
||||||
const card = page.getByLabel('Create new document card').first();
|
const card = page.getByRole('dialog').first();
|
||||||
|
|
||||||
await expect(card.getByLabel('Document name')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(card.getByLabel('icon group')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
card.getByRole('heading', {
|
card.locator('h2').getByText('Create a new document'),
|
||||||
name: 'Name the document',
|
|
||||||
level: 3,
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(card.getByLabel('Document name')).toBeVisible();
|
||||||
|
|
||||||
await expect(card.getByText('Is it public ?')).toBeVisible();
|
await expect(card.getByText('Is it public ?')).toBeVisible();
|
||||||
|
|
||||||
@@ -35,13 +29,7 @@ test.describe('Doc Create', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(card.getByLabel('Close the modal')).toBeVisible();
|
||||||
card.getByRole('button', {
|
|
||||||
name: 'Cancel',
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL('/docs/create/');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('checks the cancel button interaction', async ({ page }) => {
|
test('checks the cancel button interaction', async ({ page }) => {
|
||||||
@@ -51,13 +39,9 @@ test.describe('Doc Create', () => {
|
|||||||
await buttonCreateHomepage.click();
|
await buttonCreateHomepage.click();
|
||||||
await expect(buttonCreateHomepage).toBeHidden();
|
await expect(buttonCreateHomepage).toBeHidden();
|
||||||
|
|
||||||
const card = page.getByLabel('Create new document card').first();
|
const card = page.getByRole('dialog').first();
|
||||||
|
|
||||||
await card
|
await card.getByLabel('Close the modal').click();
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Cancel',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(buttonCreateHomepage).toBeVisible();
|
await expect(buttonCreateHomepage).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Doc } from '../types';
|
|||||||
|
|
||||||
import { KEY_LIST_DOC } from './useDocs';
|
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 ({
|
export const createDoc = async ({
|
||||||
title,
|
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 './ModalRemoveDoc';
|
||||||
export * from './ModalUpdateDoc';
|
export * from './ModalCreateUpdateDoc';
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import { Button } from '@openfun/cunningham-react';
|
import { Button } from '@openfun/cunningham-react';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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';
|
import { DocsGrid } from './DocsGrid';
|
||||||
|
|
||||||
export const DocsGridContainer = () => {
|
export const DocsGridContainer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isModalCreateOpen, setIsModalCreateOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $overflow="auto">
|
<Box $overflow="auto">
|
||||||
<Card $margin="big" $padding="tiny">
|
<Card $margin="big" $padding="tiny">
|
||||||
<Box $align="flex-end" $justify="center">
|
<Box $align="flex-end" $justify="center">
|
||||||
<StyledLink href="/docs/create">
|
<Button
|
||||||
<Button>{t('Create a new document')}</Button>
|
onClick={() => {
|
||||||
</StyledLink>
|
setIsModalCreateOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Create a new document')}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
<DocsGrid />
|
<DocsGrid />
|
||||||
|
{isModalCreateOpen && (
|
||||||
|
<ModalCreateDoc onClose={() => setIsModalCreateOpen(false)} />
|
||||||
|
)}
|
||||||
</Box>
|
</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