diff --git a/client/components/Page/Page.tsx b/client/components/Page/Page.tsx index b05ead67203..801c4bce89f 100644 --- a/client/components/Page/Page.tsx +++ b/client/components/Page/Page.tsx @@ -1,9 +1,9 @@ import { Box } from '@rocket.chat/fuselage'; -import React, { useState, FC } from 'react'; +import React, { useState, ReactElement, ComponentProps } from 'react'; import PageContext from './PageContext'; -const Page: FC = (props) => { +const Page = (props: ComponentProps): ReactElement => { const [border, setBorder] = useState(false); return ( diff --git a/client/views/admin/customEmoji/AddCustomEmoji.js b/client/views/admin/customEmoji/AddCustomEmoji.tsx similarity index 56% rename from client/views/admin/customEmoji/AddCustomEmoji.js rename to client/views/admin/customEmoji/AddCustomEmoji.tsx index 3e34406b020..7c1f469b61b 100644 --- a/client/views/admin/customEmoji/AddCustomEmoji.js +++ b/client/views/admin/customEmoji/AddCustomEmoji.tsx @@ -1,23 +1,29 @@ import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon } from '@rocket.chat/fuselage'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, ReactElement, ChangeEvent } from 'react'; import VerticalBar from '../../../components/VerticalBar'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useEndpointUpload } from '../../../hooks/useEndpointUpload'; import { useFileInput } from '../../../hooks/useFileInput'; -function AddCustomEmoji({ close, onChange, ...props }) { - const t = useTranslation(); +type AddCustomEmojiProps = { + close: () => void; + onChange: () => void; +}; +const AddCustomEmoji = ({ close, onChange, ...props }: AddCustomEmojiProps): ReactElement => { + const t = useTranslation(); const [name, setName] = useState(''); const [aliases, setAliases] = useState(''); - const [emojiFile, setEmojiFile] = useState(); + const [emojiFile, setEmojiFile] = useState(); const [newEmojiPreview, setNewEmojiPreview] = useState(''); + const [errors, setErrors] = useState({ name: false, emoji: false, aliases: false }); const setEmojiPreview = useCallback( async (file) => { setEmojiFile(file); setNewEmojiPreview(URL.createObjectURL(file)); + setErrors((prevState) => ({ ...prevState, emoji: false })); }, [setEmojiFile], ); @@ -29,11 +35,23 @@ function AddCustomEmoji({ close, onChange, ...props }) { ); const handleSave = useCallback(async () => { + if (!name) { + return setErrors((prevState) => ({ ...prevState, name: true })); + } + + if (name === aliases) { + return setErrors((prevState) => ({ ...prevState, aliases: true })); + } + + if (!emojiFile) { + return setErrors((prevState) => ({ ...prevState, emoji: true })); + } + const formData = new FormData(); formData.append('emoji', emojiFile); formData.append('name', name); formData.append('aliases', aliases); - const result = await saveAction(formData); + const result = (await saveAction(formData)) as { success: boolean }; if (result.success) { onChange(); @@ -43,27 +61,39 @@ function AddCustomEmoji({ close, onChange, ...props }) { const [clickUpload] = useFileInput(setEmojiPreview, 'emoji'); + const handleChangeName = (e: ChangeEvent): void => { + if (e.currentTarget.value !== '') { + setErrors((prevState) => ({ ...prevState, name: false })); + } + + return setName(e.currentTarget.value); + }; + + const handleChangeAliases = (e: ChangeEvent): void => { + if (e.currentTarget.value !== name) { + setErrors((prevState) => ({ ...prevState, aliases: false })); + } + + return setAliases(e.currentTarget.value); + }; + return ( {t('Name')} - setName(e.currentTarget.value)} - placeholder={t('Name')} - /> + + {errors.name && ( + {t('error-the-field-is-required', { field: t('Name') })} + )} {t('Aliases')} - setAliases(e.currentTarget.value)} - placeholder={t('Aliases')} - /> + + {errors.aliases && {t('Custom_Emoji_Error_Same_Name_And_Alias')}} + {errors.emoji && ( + + {t('error-the-field-is-required', { field: t('Custom_Emoji') })} + + )} {newEmojiPreview && ( @@ -101,18 +136,8 @@ function AddCustomEmoji({ close, onChange, ...props }) { - - - - - - - ); -} +}; export default AddCustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmoji.js b/client/views/admin/customEmoji/CustomEmoji.js deleted file mode 100644 index 36af370ff61..00000000000 --- a/client/views/admin/customEmoji/CustomEmoji.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Box, Table } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; - -import FilterByText from '../../../components/FilterByText'; -import GenericTable from '../../../components/GenericTable'; -import { useTranslation } from '../../../contexts/TranslationContext'; - -function CustomEmoji({ data, sort, onClick, onHeaderClick, setParams, params }) { - const t = useTranslation(); - - const header = useMemo( - () => [ - - {t('Name')} - , - - {t('Aliases')} - , - ], - [onHeaderClick, sort, t], - ); - - const renderRow = (emojis) => { - const { _id, name, aliases } = emojis; - return ( - - - {name} - - - {aliases} - - - ); - }; - - return ( - } - /> - ); -} - -export default CustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmojiRoute.js b/client/views/admin/customEmoji/CustomEmojiRoute.tsx similarity index 84% rename from client/views/admin/customEmoji/CustomEmojiRoute.js rename to client/views/admin/customEmoji/CustomEmojiRoute.tsx index 36cb32d84a6..ae07581117f 100644 --- a/client/views/admin/customEmoji/CustomEmojiRoute.js +++ b/client/views/admin/customEmoji/CustomEmojiRoute.tsx @@ -1,5 +1,5 @@ import { Button, Icon } from '@rocket.chat/fuselage'; -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, ReactElement } from 'react'; import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import Page from '../../../components/Page'; @@ -11,25 +11,25 @@ import AddCustomEmoji from './AddCustomEmoji'; import CustomEmoji from './CustomEmoji'; import EditCustomEmojiWithData from './EditCustomEmojiWithData'; -function CustomEmojiRoute() { +const CustomEmojiRoute = (): ReactElement => { + const t = useTranslation(); const route = useRoute('emoji-custom'); const context = useRouteParameter('context'); const id = useRouteParameter('id'); const canManageEmoji = usePermission('manage-emoji'); - const t = useTranslation(); - const handleItemClick = (_id) => () => { + const handleItemClick = (_id: string) => (): void => { route.push({ context: 'edit', id: _id, }); }; - const handleNewButtonClick = useCallback(() => { + const handleAddEmoji = useCallback(() => { route.push({ context: 'new' }); }, [route]); - const handleClose = () => { + const handleClose = (): void => { route.push({}); }; @@ -47,7 +47,7 @@ function CustomEmojiRoute() { - @@ -62,7 +62,7 @@ function CustomEmojiRoute() { {context === 'new' && t('Custom_Emoji_Add')} - {context === 'edit' && ( + {context === 'edit' && id && ( )} {context === 'new' && } @@ -70,6 +70,6 @@ function CustomEmojiRoute() { )} ); -} +}; export default CustomEmojiRoute; diff --git a/client/views/admin/customEmoji/EditCustomEmoji.tsx b/client/views/admin/customEmoji/EditCustomEmoji.tsx index 66a28c543db..62e96afb804 100644 --- a/client/views/admin/customEmoji/EditCustomEmoji.tsx +++ b/client/views/admin/customEmoji/EditCustomEmoji.tsx @@ -1,4 +1,13 @@ -import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon } from '@rocket.chat/fuselage'; +import { + Box, + Button, + ButtonGroup, + Margins, + TextInput, + Field, + Icon, + FieldGroup, +} from '@rocket.chat/fuselage'; import React, { useCallback, useState, useMemo, useEffect, FC, ChangeEvent } from 'react'; import GenericModal from '../../../components/GenericModal'; @@ -27,6 +36,7 @@ const EditCustomEmoji: FC = ({ close, onChange, data, ...p const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); const absoluteUrl = useAbsoluteUrl(); + const [errors, setErrors] = useState({ name: false, aliases: false }); const { _id, name: previousName, aliases: previousAliases } = data || {}; @@ -62,20 +72,29 @@ const EditCustomEmoji: FC = ({ close, onChange, data, ...p ); const handleSave = useCallback(async () => { - if (!emojiFile) { + if (!name) { + return setErrors((prevState) => ({ ...prevState, name: true })); + } + + if (name === aliases) { + return setErrors((prevState) => ({ ...prevState, aliases: true })); + } + + if (!emojiFile && !newEmojiPreview) { return; } const formData = new FormData(); - formData.append('emoji', emojiFile); + emojiFile && formData.append('emoji', emojiFile); formData.append('_id', _id); formData.append('name', name); formData.append('aliases', aliases); const result = (await saveAction(formData)) as { success: boolean }; if (result.success) { onChange(); + close(); } - }, [emojiFile, _id, name, aliases, saveAction, onChange]); + }, [emojiFile, _id, name, aliases, saveAction, onChange, close, newEmojiPreview]); const deleteAction = useEndpointAction( 'POST', @@ -84,23 +103,16 @@ const EditCustomEmoji: FC = ({ close, onChange, data, ...p ); const handleDeleteButtonClick = useCallback(() => { - const handleClose = (): void => { - setModal(null); - close(); - onChange(); - }; - const handleDelete = async (): Promise => { try { await deleteAction(); - setModal(() => ( - - {t('Custom_Emoji_Has_Been_Deleted')} - - )); + dispatchToastMessage({ type: 'success', message: t('Custom_Emoji_Has_Been_Deleted') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); + } finally { onChange(); + setModal(null); + close(); } }; @@ -119,76 +131,90 @@ const EditCustomEmoji: FC = ({ close, onChange, data, ...p {t('Custom_Emoji_Delete_Warning')} )); - }, [close, deleteAction, dispatchToastMessage, onChange, setModal, t]); + }, [deleteAction, close, dispatchToastMessage, onChange, setModal, t]); - const handleAliasesChange = useCallback((e) => setAliases(e.currentTarget.value), [setAliases]); + const handleChangeAliases = useCallback( + (e) => { + if (e.currentTarget.value !== name) { + setErrors((prevState) => ({ ...prevState, aliases: false })); + } + + return setAliases(e.currentTarget.value); + }, + [setAliases, name], + ); const [clickUpload] = useFileInput(setEmojiFile, 'emoji'); + const handleChangeName = (e: ChangeEvent): void => { + if (e.currentTarget.value !== '') { + setErrors((prevState) => ({ ...prevState, name: false })); + } + + return setName(e.currentTarget.value); + }; + return ( - - {t('Name')} - - ): void => setName(e.currentTarget.value)} - placeholder={t('Name')} - /> - - - - {t('Aliases')} - - - - - - - {t('Custom_Emoji')} - - - {newEmojiPreview && ( - - - - - - )} - - - - - - - - - - - - - - - - + + {newEmojiPreview && ( + + + + + + )} + + + + + + + + + + ); }; diff --git a/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx b/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx index 0f7c1bc1d2b..8f859e364c9 100644 --- a/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx +++ b/client/views/admin/customEmoji/EditCustomEmojiWithData.tsx @@ -1,4 +1,12 @@ -import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage'; +import { + Box, + Button, + ButtonGroup, + Skeleton, + Throbber, + InputBox, + Callout, +} from '@rocket.chat/fuselage'; import React, { useMemo, FC } from 'react'; import { useTranslation } from '../../../contexts/TranslationContext'; @@ -12,7 +20,12 @@ type EditCustomEmojiWithDataProps = { onChange: () => void; }; -const EditCustomEmojiWithData: FC = ({ _id, onChange, ...props }) => { +const EditCustomEmojiWithData: FC = ({ + _id, + onChange, + close, + ...props +}) => { const t = useTranslation(); const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]); @@ -52,11 +65,7 @@ const EditCustomEmojiWithData: FC = ({ _id, onChan } if (error || !data || !data.emojis || data.emojis.update.length < 1) { - return ( - - {t('Custom_User_Status_Error_Invalid_User_Status')} - - ); + return ; } const handleChange = (): void => { @@ -64,7 +73,14 @@ const EditCustomEmojiWithData: FC = ({ _id, onChan reload && reload(); }; - return ; + return ( + + ); }; export default EditCustomEmojiWithData; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 4e8482bd4cc..55d0537667e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1296,6 +1296,7 @@ "Custom_Emoji_Delete_Warning": "Deleting an emoji cannot be undone.", "Custom_Emoji_Error_Invalid_Emoji": "Invalid emoji", "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "The custom emoji or one of its aliases is already in use.", + "Custom_Emoji_Error_Same_Name_And_Alias": "The custom emoji name and their aliases should be different.", "Custom_Emoji_Has_Been_Deleted": "The custom emoji has been deleted.", "Custom_Emoji_Info": "Custom Emoji Info", "Custom_Emoji_Updated_Successfully": "Custom emoji updated successfully",