diff --git a/src/components/item/edit/EditModal.tsx b/src/components/item/edit/EditModal.tsx index a69cf73bf..0f92c6fec 100644 --- a/src/components/item/edit/EditModal.tsx +++ b/src/components/item/edit/EditModal.tsx @@ -30,15 +30,14 @@ import { BUILDER } from '../../../langs/constants'; import BaseItemForm from '../form/BaseItemForm'; import FileForm from '../form/FileForm'; import FolderForm from '../form/FolderForm'; -import NameForm from '../form/NameForm'; import DocumentForm from '../form/document/DocumentForm'; +import EditShortcutForm from '../shortcut/EditShortcutForm'; const { editItemRoutine } = routines; export interface EditModalContentPropType { item?: DiscriminatedItem; setChanges: (payload: Partial) => void; - updatedProperties: Partial; } export type EditModalContentType = CT; @@ -68,20 +67,13 @@ const EditModal = ({ item, onClose, open }: Props): JSX.Element => { setUpdatedItem({ ...updatedItem, ...payload } as DiscriminatedItem); }; - const renderComponent = (): JSX.Element => { + // files are handled beforehand + const renderDialogContent = (): JSX.Element => { switch (item.type) { case ItemType.DOCUMENT: return ; - case ItemType.LOCAL_FILE: - case ItemType.S3_FILE: - return ; case ItemType.SHORTCUT: - return ( - - ); + return ; case ItemType.FOLDER: return ; case ItemType.LINK: @@ -131,6 +123,48 @@ const EditModal = ({ item, onClose, open }: Props): JSX.Element => { onClose(); }; + // temporary solution for displaying separate dialog content + const renderContent = () => { + if (item.type === ItemType.LOCAL_FILE || item.type === ItemType.S3_FILE) { + return ; + } + + return ( + <> + + {renderDialogContent()} + + + + + + + ); + }; + return ( { {translateBuilder(BUILDER.EDIT_ITEM_MODAL_TITLE)} - - {renderComponent()} - - - - - + {renderContent()} ); }; diff --git a/src/components/item/form/FileForm.tsx b/src/components/item/form/FileForm.tsx index 9b2668513..937bdbefb 100644 --- a/src/components/item/form/FileForm.tsx +++ b/src/components/item/form/FileForm.tsx @@ -1,11 +1,16 @@ -import { useEffect } from 'react'; +import { ReactNode } from 'react'; import { useForm } from 'react-hook-form'; -import { TextField } from '@mui/material'; +import { + Box, + Button, + DialogActions, + DialogContent, + TextField, +} from '@mui/material'; import { DescriptionPlacementType, - DiscriminatedItem, ItemType, LocalFileItemExtra, LocalFileItemType, @@ -13,13 +18,19 @@ import { S3FileItemExtra, S3FileItemType, } from '@graasp/sdk'; +import { COMMON } from '@graasp/translations'; -import { useBuilderTranslation } from '@/config/i18n'; -import { ITEM_FORM_IMAGE_ALT_TEXT_EDIT_FIELD_ID } from '@/config/selectors'; +import CancelButton from '@/components/common/CancelButton'; +import { useBuilderTranslation, useCommonTranslation } from '@/config/i18n'; +import { mutations } from '@/config/queryClient'; +import { + EDIT_ITEM_MODAL_CANCEL_BUTTON_ID, + ITEM_FORM_CONFIRM_BUTTON_ID, + ITEM_FORM_IMAGE_ALT_TEXT_EDIT_FIELD_ID, +} from '@/config/selectors'; import { getExtraFromPartial } from '@/utils/itemExtra'; import { BUILDER } from '../../../langs/constants'; -import type { EditModalContentPropType } from '../edit/EditModal'; import DescriptionForm from './DescriptionForm'; import NameForm from './NameForm'; @@ -32,60 +43,94 @@ type Inputs = { const FileForm = ({ item, - setChanges, + onClose, }: { - item: DiscriminatedItem; - setChanges: EditModalContentPropType['setChanges']; -}): JSX.Element | null => { + item: LocalFileItemType | S3FileItemType; + onClose: () => void; +}): ReactNode => { const { t: translateBuilder } = useBuilderTranslation(); - const { register, watch, setValue } = useForm(); + const { t: translateCommon } = useCommonTranslation(); + const { + register, + watch, + setValue, + handleSubmit, + reset, + formState: { errors, isValid }, + } = useForm(); const altText = watch('altText'); const description = watch('description'); const descriptionPlacement = watch('descriptionPlacement'); - const name = watch('name'); - useEffect(() => { - let newExtra: S3FileItemExtra | LocalFileItemExtra | undefined; + const { mimetype, altText: previousAltText } = getExtraFromPartial(item); - if (item.type === ItemType.S3_FILE) { - newExtra = { - [ItemType.S3_FILE]: { - ...item.extra[ItemType.S3_FILE], - altText, - }, - }; - } else if (item.type === ItemType.LOCAL_FILE) { - newExtra = { - [ItemType.LOCAL_FILE]: { - ...item.extra[ItemType.LOCAL_FILE], - altText, - }, - }; - } + const { mutateAsync: editItem } = mutations.useEditItem(); - setChanges({ - name, - description, - settings: { descriptionPlacement }, - extra: newExtra, - } as S3FileItemType | LocalFileItemType); + function buildFileExtra() { + if (altText) { + if (item.type === ItemType.S3_FILE) { + return { + [ItemType.S3_FILE]: { + altText, + }, + } as S3FileItemExtra; + } + if (item.type === ItemType.LOCAL_FILE) { + return { + [ItemType.LOCAL_FILE]: { + altText, + }, + } as LocalFileItemExtra; + } + } + console.error(`item type ${item.type} is not handled`); + return undefined; + } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [altText, description, descriptionPlacement, name, setChanges]); + async function onSubmit(data: Inputs) { + try { + await editItem({ + id: item.id, + name: data.name, + description: data.description, + // only post extra if it has been changed + extra: altText !== previousAltText ? buildFileExtra() : undefined, + // only patch settings it it has been changed + settings: + descriptionPlacement !== item.settings.descriptionPlacement + ? { descriptionPlacement } + : undefined, + }); + onClose(); + } catch (e) { + console.error(e); + } + } - if (item) { - const itemExtra = getExtraFromPartial(item); - const { mimetype, altText: previousAltText } = itemExtra; - return ( - <> - + return ( + + + reset({ name: '' })} + /> {mimetype && MimeTypes.isImage(mimetype) && ( - - ); - } - return null; + + + + + + + ); }; export default FileForm; diff --git a/src/components/item/form/NameForm.tsx b/src/components/item/form/NameForm.tsx index 9322209f2..bf53aa681 100644 --- a/src/components/item/form/NameForm.tsx +++ b/src/components/item/form/NameForm.tsx @@ -1,5 +1,4 @@ -import { ChangeEvent } from 'react'; -import { UseFormRegisterReturn } from 'react-hook-form'; +import { FieldError, UseFormRegisterReturn } from 'react-hook-form'; import ClearIcon from '@mui/icons-material/Clear'; import { IconButton, TextField } from '@mui/material'; @@ -26,29 +25,25 @@ export type NameFormProps = { */ name?: string; autoFocus?: boolean; - nameForm?: UseFormRegisterReturn; + nameForm: UseFormRegisterReturn; reset?: () => void; + error?: FieldError; + showClearButton?: boolean; }; const NameForm = ({ nameForm, required, - setChanges, - name, autoFocus = true, reset, + error, + showClearButton, }: NameFormProps): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); - const handleNameInput = (event: ChangeEvent<{ value: string }>) => { - setChanges?.({ name: event.target.value, displayName: event.target.value }); - }; - const handleClearClick = () => { reset?.(); - setChanges?.({ name: '' }); }; - return ( - - - ), + slotProps={{ + inputLabel: { shrink: true }, + input: { + // add a clear icon button + endAdornment: ( + + + + ), + }, }} // only take full width when on small screen size fullWidth sx={{ my: 1 }} - // TODO: to remove when all components using NameForm move to react hook form - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - onChange={handleNameInput} - value={name} - {...(nameForm ?? {})} + error={Boolean(error)} + helperText={error?.message} + {...nameForm} /> ); }; diff --git a/src/components/item/shortcut/EditShortcutForm.tsx b/src/components/item/shortcut/EditShortcutForm.tsx new file mode 100644 index 000000000..022f1f8cd --- /dev/null +++ b/src/components/item/shortcut/EditShortcutForm.tsx @@ -0,0 +1,39 @@ +import { ReactNode, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import NameForm from '../form/NameForm'; + +type Inputs = { + name: string; +}; + +function EditShortcutForm({ + item, + setChanges, +}: { + item: DiscriminatedItem; + setChanges: (args: { name: string }) => void; +}): ReactNode { + const { register, reset, watch } = useForm(); + + const name = watch('name'); + + // synchronize upper state after async local state change + useEffect(() => { + setChanges({ + name, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [name]); + + return ( + reset({ name: '' })} + nameForm={register('name', { value: item.name })} + /> + ); +} + +export default EditShortcutForm; diff --git a/src/utils/item.ts b/src/utils/item.ts index cbbbe5ec5..596f59f0d 100644 --- a/src/utils/item.ts +++ b/src/utils/item.ts @@ -60,6 +60,10 @@ export const isUrlValid = (str?: string | null): boolean => { return pattern.test(str); }; +/** + * + * @deprecated + */ export const isItemValid = (item: Partial): boolean => { if (!item) { return false;