From 760b77a2934cd326061f008b5002b3f2524d086b Mon Sep 17 00:00:00 2001 From: Kim Lan Phan Hoang Date: Fri, 25 Oct 2024 11:39:04 +0200 Subject: [PATCH] refactor: use react hook form for link create modal (#1545) * refactor: use react hook form for link create modal * refactor: apply PR requested changes --- .../item/form/link/LinkDescriptionField.tsx | 23 +- src/components/item/form/link/LinkForm.tsx | 293 ++++++++---------- .../item/form/link/LinkUrlField.tsx | 22 +- src/components/main/NewItemModal.tsx | 5 +- 4 files changed, 146 insertions(+), 197 deletions(-) diff --git a/src/components/item/form/link/LinkDescriptionField.tsx b/src/components/item/form/link/LinkDescriptionField.tsx index 75c36d8b1..65f314ba6 100644 --- a/src/components/item/form/link/LinkDescriptionField.tsx +++ b/src/components/item/form/link/LinkDescriptionField.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react'; +import { UseFormRegisterReturn } from 'react-hook-form'; import { IconButton, TextField } from '@mui/material'; @@ -8,18 +8,18 @@ import { useBuilderTranslation } from '@/config/i18n'; import { BUILDER } from '@/langs/constants'; type LinkDescriptionFieldProps = { - value: string | null | undefined; - onChange: (newValue: string) => void; onRestore: () => void; onClear: () => void; - showRestore: boolean; + showRestoreButton?: boolean; + form: UseFormRegisterReturn; + showClearButton?: boolean; }; const LinkDescriptionField = ({ - value, - onChange, + form, onRestore, onClear, - showRestore, + showRestoreButton, + showClearButton, }: LinkDescriptionFieldProps): JSX.Element => { const { t } = useBuilderTranslation(); return ( @@ -27,17 +27,14 @@ const LinkDescriptionField = ({ label={t(BUILDER.DESCRIPTION_LABEL)} variant="standard" InputLabelProps={{ shrink: true }} - value={value} - onChange={({ - target: { value: newValue }, - }: ChangeEvent) => onChange(newValue)} + {...form} InputProps={{ endAdornment: ( <> @@ -46,7 +43,7 @@ const LinkDescriptionField = ({ diff --git a/src/components/item/form/link/LinkForm.tsx b/src/components/item/form/link/LinkForm.tsx index 155665f9a..cdfb3e582 100644 --- a/src/components/item/form/link/LinkForm.tsx +++ b/src/components/item/form/link/LinkForm.tsx @@ -1,4 +1,5 @@ -import { ChangeEvent, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { FormControl, @@ -16,8 +17,8 @@ import { DiscriminatedItem, ItemType, LinkItemType, + UnionOfConst, buildLinkExtra, - getLinkExtra, getLinkThumbnailUrl, } from '@graasp/sdk'; import { LinkCard, LinkItem } from '@graasp/ui'; @@ -30,17 +31,11 @@ import { isUrlValid } from '../../../../utils/item'; import NameForm from '../NameForm'; import LinkDescriptionField from './LinkDescriptionField'; import LinkUrlField from './LinkUrlField'; -import { - LinkType, - getLinkType, - getSettingsFromLinkType, - normalizeURL, -} from './linkUtils'; +import { LinkType, getSettingsFromLinkType, normalizeURL } from './linkUtils'; type Props = { onChange: (item: Partial) => void; item?: LinkItemType; - updatedProperties: Partial; }; const StyledFormControlLabel = styled(FormControlLabel)(({ theme }) => ({ @@ -62,97 +57,56 @@ const StyledDiv = styled('div')(() => ({ }, })); -const LinkForm = ({ - onChange, - item, - updatedProperties, -}: Props): JSX.Element => { - const [linkContent, setLinkContent] = useState(''); - const [isDescriptionDirty, setIsDescriptionDirty] = useState(false); - const { t: translateBuilder } = useBuilderTranslation(); - const { data: linkData } = hooks.useLinkMetadata(normalizeURL(linkContent)); - - // get value from the updatedProperties - const linkType = getLinkType(updatedProperties.settings); - - const handleLinkInput = (value: string) => { - setLinkContent(value); - onChange({ - extra: buildLinkExtra({ - // when used inside the NewItem Modal this component does not receive the item prop - // so the https will not show, but it will be added when we submit the url. - url: normalizeURL(value), - html: '', - thumbnails: [], - icons: [], - }), - }); - }; - - let url = ''; - let description: string | undefined = ''; - const extraProps = updatedProperties.extra; - if (extraProps && ItemType.LINK in extraProps) { - ({ url, description } = getLinkExtra(extraProps) || {}); - } - // link is considered valid if it is either empty, or it is a valid url - const isLinkValid = linkContent.length === 0 || isUrlValid(url); - - const onChangeLinkType = ({ - target: { value }, - }: ChangeEvent) => { - const settings = getSettingsFromLinkType(value); - onChange({ settings }); - }; +type Inputs = { + name: string; + linkType: UnionOfConst; + description: string; + url: string; +}; - const onClickClearURL = () => { - setLinkContent(''); - }; - const onClickClearDescription = () => { - onChange({ description: '' }); - setIsDescriptionDirty(false); - }; - const onClickRestoreDefaultDescription = () => { - onChange({ description: linkData?.description }); - setIsDescriptionDirty(false); - }; - const onChangeDescription = (value: string) => { - setIsDescriptionDirty(true); - onChange({ description: value }); - }; +const LinkForm = ({ onChange, item }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const { register, watch, control, setValue, reset } = useForm(); + const url = watch('url'); + + const name = watch('name'); + const linkType = watch('linkType'); + const description = watch('description'); + const { data: linkData } = hooks.useLinkMetadata( + isUrlValid(normalizeURL(url)) ? normalizeURL(url) : '', + ); // apply the description from the api to the field // this is only run once. useEffect( () => { - // this is the object on which we will define the props to be updated - const updatedProps: Partial = {}; - - if (!isDescriptionDirty && linkData?.description) { - updatedProps.description = linkData?.description; - } - if (linkData?.title) { - updatedProps.name = linkData.title; + if (!description && linkData?.description) { + setValue('description', linkData.description); } - if (linkData?.description) { - updatedProps.extra = buildLinkExtra({ - ...updatedProperties.extra?.embeddedLink, - url: updatedProperties.extra?.embeddedLink.url || '', - description: linkData.description, - thumbnails: linkData.thumbnails, - icons: linkData.icons, - }); - } - // update props in one call to remove issue of race updates - if (Object.keys(updatedProps).length) { - // this should be called only once ! - onChange(updatedProps); + if (!name && linkData?.title) { + setValue('name', linkData.title); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [linkData], ); + // synchronize upper state after async local state change + useEffect(() => { + onChange({ + name, + description, + extra: buildLinkExtra({ + url, + description: linkData?.description, + thumbnails: linkData?.thumbnails, + icons: linkData?.icons, + }), + settings: getSettingsFromLinkType(linkType), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [description, linkData, linkType, name, url]); + const embeddedLinkPreview = useMemo( () => ( reset({ url: '' })} + showClearButton={Boolean(url)} + isValid={isUrlValid(normalizeURL(url))} /> reset({ name: '' })} /> setValue('description', linkData?.description ?? '')} + form={register('description')} + onClear={() => reset({ description: '' })} + showRestoreButton={ + Boolean(description) && description !== linkData?.description + } + showClearButton={Boolean(description)} /> {translateBuilder(BUILDER.CREATE_ITEM_LINK_TYPE_TITLE)} - {linkContent ? ( - - {linkContent}} - control={} - /> - ( + + {url}} + control={} /> - } - control={} - slotProps={{ typography: { width: '100%', minWidth: '0px' } }} - sx={{ minWidth: '0px', width: '100%' }} - /> - {linkData?.html && linkData.html !== '' && ( - - } - control={} - slotProps={{ - typography: { - width: '100%', - minWidth: '0px', - }, - }} - /> - )} - { - // only show this options when embedding is allowed and there is no html code - // as the html will take precedence over showing the site as an iframe - // and some sites like daily motion actually allow both, we want to allow show the html setting - linkData?.isEmbeddingAllowed && !linkData?.html && ( + } control={} slotProps={{ - typography: { - width: '100%', - minWidth: '0px', - }, - }} - sx={{ - // this ensure the iframe takes up all horizontal space - '& iframe': { - width: '100%', - }, + typography: { width: '100%', minWidth: '0px' }, }} + sx={{ minWidth: '0px', width: '100%' }} /> - ) - } - + {linkData?.html && linkData.html !== '' && ( + + } + control={} + slotProps={{ + typography: { + width: '100%', + minWidth: '0px', + }, + }} + /> + )} + { + // only show this options when embedding is allowed and there is no html code + // as the html will take precedence over showing the site as an iframe + // and some sites like daily motion actually allow both, we want to allow show the html setting + linkData?.isEmbeddingAllowed && !linkData?.html && ( + } + slotProps={{ + typography: { + width: '100%', + minWidth: '0px', + }, + }} + sx={{ + // this ensure the iframe takes up all horizontal space + '& iframe': { + width: '100%', + }, + }} + /> + ) + } + + )} + /> ) : ( {translateBuilder(BUILDER.CREATE_ITEM_LINK_TYPE_HELPER_TEXT)} diff --git a/src/components/item/form/link/LinkUrlField.tsx b/src/components/item/form/link/LinkUrlField.tsx index 5b312b9cc..95b1f4689 100644 --- a/src/components/item/form/link/LinkUrlField.tsx +++ b/src/components/item/form/link/LinkUrlField.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react'; +import { UseFormRegisterReturn } from 'react-hook-form'; import { IconButton, TextField } from '@mui/material'; @@ -9,30 +9,26 @@ import { ITEM_FORM_LINK_INPUT_ID } from '@/config/selectors'; import { BUILDER } from '@/langs/constants'; type LinkUrlFieldProps = { - value: string; - isValid: boolean; onClear: () => void; - onChange: (newValue: string) => void; + form: UseFormRegisterReturn; + showClearButton?: boolean; + isValid: boolean; }; const LinkUrlField = ({ - value, - isValid, onClear, - onChange, + form, + showClearButton = false, + isValid, }: LinkUrlFieldProps): JSX.Element => { const { t } = useBuilderTranslation(); return ( ) => onChange(newValue)} + {...form} helperText={isValid ? '' : t(BUILDER.CREATE_ITEM_LINK_INVALID_LINK_ERROR)} InputLabelProps={{ shrink: true }} InputProps={{ @@ -40,7 +36,7 @@ const LinkUrlField = ({ diff --git a/src/components/main/NewItemModal.tsx b/src/components/main/NewItemModal.tsx index e6cf0ac11..d5c77d289 100644 --- a/src/components/main/NewItemModal.tsx +++ b/src/components/main/NewItemModal.tsx @@ -231,10 +231,7 @@ const NewItemModal = ({ {translateBuilder(BUILDER.CREATE_ITEM_LINK_TITLE)} - + ); case ItemType.DOCUMENT: