Skip to content

Commit

Permalink
refactor: use react hook form for document forms (#1580)
Browse files Browse the repository at this point in the history
* refactor: use react hook form for document forms

* refactor: fix tests

* refactor: apply PR requested changes
  • Loading branch information
pyphilia authored Nov 26, 2024
1 parent 8410baf commit bfe4205
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 322 deletions.
31 changes: 29 additions & 2 deletions cypress/e2e/item/create/createDocument.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,35 @@ describe('Create Document', () => {
cy.visit(HOME_PATH);

// create
createDocument(DocumentItemFactory());
const document = DocumentItemFactory({
extra: { document: { content: 'my content' } },
});
createDocument(document);

cy.wait('@postItem').then(({ request: { body } }) => {
expect(body.extra.document.content).to.contain(
document.extra.document.content,
);
// should update view
cy.wait('@getAccessibleItems');
});
});

it('create html document on Home', () => {
cy.setUpApi();
cy.visit(HOME_PATH);

// create
const document = DocumentItemFactory({
extra: { document: { content: 'my content', isRaw: true } },
});
createDocument(document);

cy.wait('@postItem').then(() => {
cy.wait('@postItem').then(({ request: { body } }) => {
expect(body.extra.document.isRaw).to.equal(true);
expect(body.extra.document.content).to.equal(
document.extra.document.content,
);
// should update view
cy.wait('@getAccessibleItems');
});
Expand Down Expand Up @@ -60,6 +86,7 @@ describe('Create Document', () => {
{ confirm: false },
);

cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).click();
cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).should(
'have.prop',
'disabled',
Expand Down
12 changes: 9 additions & 3 deletions cypress/support/commands/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,16 @@ Cypress.Commands.add(
({ name = '', extra }, { confirm = true } = {}) => {
cy.fillBaseItemModal({ name }, { confirm: false });

cy.get(ITEM_FORM_DOCUMENT_TEXT_SELECTOR).type(
const content =
// first select all the text and then remove it to have a clear field, then type new text
`{selectall}{backspace}${getDocumentExtra(extra)?.content}`,
);
`{selectall}{backspace}${getDocumentExtra(extra)?.content}`;

if (extra.document.isRaw) {
cy.get(`[role="tab"]:contains("HTML")`).click();
cy.get('[role="tabpanel"] textarea[name="content"]').type(content);
} else {
cy.get(ITEM_FORM_DOCUMENT_TEXT_SELECTOR).type(content);
}

if (confirm) {
cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).click();
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"@emotion/styled": "11.13.0",
"@graasp/chatbox": "3.3.0",
"@graasp/map": "1.19.0",
"@graasp/query-client": "5.5.0",
"@graasp/query-client": "5.6.0",
"@graasp/sdk": "5.3.1",
"@graasp/stylis-plugin-rtl": "2.2.0",
"@graasp/translations": "1.41.0",
"@graasp/translations": "1.42.0",
"@graasp/ui": "5.4.2",
"@mui/icons-material": "6.1.7",
"@mui/lab": "6.0.0-beta.15",
Expand Down
9 changes: 5 additions & 4 deletions src/components/item/edit/EditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { isItemValid } from '@/utils/item';

import { BUILDER } from '../../../langs/constants';
import BaseItemForm from '../form/BaseItemForm';
import DocumentForm from '../form/document/DocumentForm';
import { DocumentEditForm } from '../form/document/DocumentEditForm';
import FileForm from '../form/file/FileForm';
import { FolderEditForm } from '../form/folder/FolderEditForm';
import EditShortcutForm from '../shortcut/EditShortcutForm';
Expand Down Expand Up @@ -67,11 +67,9 @@ const EditModal = ({ item, onClose, open }: Props): JSX.Element => {
setUpdatedItem({ ...updatedItem, ...payload } as DiscriminatedItem);
};

// files and folders are handled beforehand
// files, folders, documents are handled beforehand
const renderDialogContent = (): JSX.Element => {
switch (item.type) {
case ItemType.DOCUMENT:
return <DocumentForm setChanges={setChanges} item={item} />;
case ItemType.LINK:
case ItemType.APP:
case ItemType.ETHERPAD:
Expand Down Expand Up @@ -129,6 +127,9 @@ const EditModal = ({ item, onClose, open }: Props): JSX.Element => {
if (item.type === ItemType.SHORTCUT) {
return <EditShortcutForm onClose={onClose} item={item} />;
}
if (item.type === ItemType.DOCUMENT) {
return <DocumentEditForm onClose={onClose} item={item} />;
}

return (
<>
Expand Down
148 changes: 79 additions & 69 deletions src/components/item/form/document/DocumentContentForm.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Dispatch } from 'react';
import { UseFormRegisterReturn } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';

import { TabContext, TabList, TabPanel } from '@mui/lab';
import { Box, Stack, Tab, TextField } from '@mui/material';
import { Box, Tab, TextField, Typography } from '@mui/material';

import {
DocumentItemExtraFlavor,
DocumentItemExtraProperties,
} from '@graasp/sdk';
import { DocumentItemExtraFlavor } from '@graasp/sdk';
import { withFlavor } from '@graasp/ui';
import TextEditor from '@graasp/ui/text-editor';

Expand All @@ -21,82 +17,96 @@ export type DocumentExtraFormInputs = {
};

export const DocumentContentForm = ({
contentForm,
documentItemId,
flavor = DocumentItemExtraFlavor.None,
isRaw,
setIsRaw,
placeholder,
// content value to pass to text editor
content,
// set value to pass to text editor
onChange,
}: {
content: string;
contentForm: UseFormRegisterReturn;
documentItemId?: string;
flavor?: DocumentItemExtraProperties['flavor'];
isRaw: boolean;
onChange: (v: string) => void;
placeholder?: string;
setIsRaw: Dispatch<boolean>;
}): JSX.Element => {
const { t } = useBuilderTranslation();
const {
register,
watch,
control,
formState: { errors },
} = useFormContext<{
content: string;
isRaw: boolean;
flavor: `${DocumentItemExtraFlavor}`;
}>();

return (
<Stack direction="column" spacing={1} minHeight={0} marginTop={1}>
<TabContext
value={isRaw ? EditorMode.Raw.toString() : EditorMode.Rich.toString()}
>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList
onChange={(_, mode) => {
setIsRaw(mode === EditorMode.Raw.toString());
}}
aria-label={t(BUILDER.DOCUMENT_EDITOR_MODE_ARIA_LABEL)}
centered
variant="fullWidth"
>
<Tab
label={t(BUILDER.DOCUMENT_EDITOR_MODE_RICH_TEXT)}
value={EditorMode.Rich.toString()}
/>
<Tab
label={t(BUILDER.DOCUMENT_EDITOR_MODE_RAW)}
value={EditorMode.Raw.toString()}
/>
</TabList>
</Box>
const isRaw = watch('isRaw');
const content = watch('content');
const flavor = watch('flavor');

<TabPanel value={EditorMode.Rich.toString()}>
{withFlavor({
content: (
<TextEditor
id={documentItemId}
value={content}
onChange={onChange}
placeholderText={placeholder}
showActions={false}
return (
<TabContext
value={isRaw ? EditorMode.Raw.toString() : EditorMode.Rich.toString()}
>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Controller
name="isRaw"
control={control}
render={({ field }) => (
<TabList
onChange={(_, mode) => {
field.onChange(mode === EditorMode.Raw.toString());
}}
aria-label={t(BUILDER.DOCUMENT_EDITOR_MODE_ARIA_LABEL)}
centered
variant="fullWidth"
>
<Tab
label={t(BUILDER.DOCUMENT_EDITOR_MODE_RICH_TEXT)}
value={EditorMode.Rich.toString()}
/>
),
flavor,
})}
</TabPanel>
<TabPanel value={EditorMode.Raw.toString()} sx={{ minHeight: '0px' }}>
{withFlavor({
content: (
<TextField
multiline
fullWidth
minRows={5}
maxRows={25}
{...contentForm}
<Tab
label={t(BUILDER.DOCUMENT_EDITOR_MODE_RAW)}
value={EditorMode.Raw.toString()}
/>
),
flavor,
})}
</TabPanel>
</TabContext>
</Stack>
</TabList>
)}
/>
</Box>

<TabPanel value={EditorMode.Rich.toString()}>
{withFlavor({
content: (
<TextEditor
id={documentItemId}
value={content}
onChange={onChange}
placeholderText={placeholder}
showActions={false}
/>
),
flavor,
})}
</TabPanel>
<TabPanel value={EditorMode.Raw.toString()} sx={{ minHeight: '0px' }}>
{withFlavor({
content: (
<TextField
multiline
fullWidth
minRows={5}
maxRows={25}
{...register('content', {
required: t(BUILDER.DOCUMENT_EMPTY_MESSAGE),
minLength: 1,
})}
/>
),
flavor,
})}
</TabPanel>
<Typography variant="caption" color="error">
{errors?.content?.message}
</Typography>
</TabContext>
);
};
111 changes: 111 additions & 0 deletions src/components/item/form/document/DocumentCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { FormProvider, useForm } from 'react-hook-form';

import { Box, DialogActions, DialogContent, DialogTitle } from '@mui/material';

import {
DiscriminatedItem,
DocumentItemExtraFlavor,
ItemGeolocation,
ItemType,
buildDocumentExtra,
} from '@graasp/sdk';
import { COMMON } from '@graasp/translations';
import { Button } from '@graasp/ui';

import CancelButton from '@/components/common/CancelButton';
import { useBuilderTranslation, useCommonTranslation } from '@/config/i18n';
import { mutations } from '@/config/queryClient';
import {
ITEM_FORM_CONFIRM_BUTTON_ID,
ITEM_FORM_DOCUMENT_TEXT_ID,
} from '@/config/selectors';
import { BUILDER } from '@/langs/constants';

import { ItemNameField } from '../ItemNameField';
import {
DocumentContentForm,
DocumentExtraFormInputs,
} from './DocumentContentForm';
import { DocumentFlavorSelect } from './DocumentFlavorSelect';

type Props = {
onClose: () => void;
parentId?: DiscriminatedItem['id'];
geolocation?: Pick<ItemGeolocation, 'lat' | 'lng'>;
previousItemId?: DiscriminatedItem['id'];
};

type Inputs = {
name: string;
isRaw: boolean;
flavor: `${DocumentItemExtraFlavor}`;
} & DocumentExtraFormInputs;

export function DocumentCreateForm({
parentId,
geolocation,
previousItemId,
onClose,
}: Props): JSX.Element {
const { t: translateBuilder } = useBuilderTranslation();
const { t: translateCommon } = useCommonTranslation();
const { mutateAsync: createItem } = mutations.usePostItem();

const methods = useForm<Inputs>({
defaultValues: { flavor: DocumentItemExtraFlavor.None },
});
const {
reset,
handleSubmit,
formState: { isValid, isSubmitted },
} = methods;

async function onSubmit(data: Inputs) {
try {
await createItem({
type: ItemType.DOCUMENT,
name: data.name,
extra: buildDocumentExtra({
content: data.content,
flavor: data.flavor,
isRaw: data.isRaw,
}),
parentId,
geolocation,
previousItemId,
});
onClose();
} catch (e) {
console.error(e);
}
}

return (
<Box component="form" height="100%" onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>
{translateBuilder(BUILDER.CREATE_NEW_ITEM_DOCUMENT_TITLE)}
</DialogTitle>
<FormProvider {...methods}>
<DialogContent>
<ItemNameField required />
<DocumentFlavorSelect />
<DocumentContentForm
documentItemId={ITEM_FORM_DOCUMENT_TEXT_ID}
onChange={(v) => reset({ content: v })}
placeholder={translateBuilder(BUILDER.TEXT_EDITOR_PLACEHOLDER)}
/>
</DialogContent>
<DialogActions>
<CancelButton onClick={onClose} />
<Button
id={ITEM_FORM_CONFIRM_BUTTON_ID}
type="submit"
disabled={isSubmitted && !isValid}
>
{translateCommon(COMMON.SAVE_BUTTON)}
</Button>
</DialogActions>
</FormProvider>
</Box>
);
}
Loading

0 comments on commit bfe4205

Please sign in to comment.