Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use react hook form for document forms #1580

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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