[FIX] Custom emoji route in admin (#23882)

pull/23948/head
Sidharth Mohanty 4 years ago committed by GitHub
parent e62d8d412a
commit ac24dd5885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      client/components/Page/Page.tsx
  2. 77
      client/views/admin/customEmoji/AddCustomEmoji.tsx
  3. 65
      client/views/admin/customEmoji/CustomEmoji.js
  4. 18
      client/views/admin/customEmoji/CustomEmojiRoute.tsx
  5. 182
      client/views/admin/customEmoji/EditCustomEmoji.tsx
  6. 32
      client/views/admin/customEmoji/EditCustomEmojiWithData.tsx
  7. 1
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -1,9 +1,9 @@
import { Box } from '@rocket.chat/fuselage'; import { Box } from '@rocket.chat/fuselage';
import React, { useState, FC } from 'react'; import React, { useState, ReactElement, ComponentProps } from 'react';
import PageContext from './PageContext'; import PageContext from './PageContext';
const Page: FC = (props) => { const Page = (props: ComponentProps<typeof Box>): ReactElement => {
const [border, setBorder] = useState(false); const [border, setBorder] = useState(false);
return ( return (
<PageContext.Provider value={[border, setBorder]}> <PageContext.Provider value={[border, setBorder]}>

@ -1,23 +1,29 @@
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon } from '@rocket.chat/fuselage'; 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 VerticalBar from '../../../components/VerticalBar';
import { useTranslation } from '../../../contexts/TranslationContext'; import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointUpload } from '../../../hooks/useEndpointUpload'; import { useEndpointUpload } from '../../../hooks/useEndpointUpload';
import { useFileInput } from '../../../hooks/useFileInput'; import { useFileInput } from '../../../hooks/useFileInput';
function AddCustomEmoji({ close, onChange, ...props }) { type AddCustomEmojiProps = {
const t = useTranslation(); close: () => void;
onChange: () => void;
};
const AddCustomEmoji = ({ close, onChange, ...props }: AddCustomEmojiProps): ReactElement => {
const t = useTranslation();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [aliases, setAliases] = useState(''); const [aliases, setAliases] = useState('');
const [emojiFile, setEmojiFile] = useState(); const [emojiFile, setEmojiFile] = useState<Blob>();
const [newEmojiPreview, setNewEmojiPreview] = useState(''); const [newEmojiPreview, setNewEmojiPreview] = useState('');
const [errors, setErrors] = useState({ name: false, emoji: false, aliases: false });
const setEmojiPreview = useCallback( const setEmojiPreview = useCallback(
async (file) => { async (file) => {
setEmojiFile(file); setEmojiFile(file);
setNewEmojiPreview(URL.createObjectURL(file)); setNewEmojiPreview(URL.createObjectURL(file));
setErrors((prevState) => ({ ...prevState, emoji: false }));
}, },
[setEmojiFile], [setEmojiFile],
); );
@ -29,11 +35,23 @@ function AddCustomEmoji({ close, onChange, ...props }) {
); );
const handleSave = useCallback(async () => { 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(); const formData = new FormData();
formData.append('emoji', emojiFile); formData.append('emoji', emojiFile);
formData.append('name', name); formData.append('name', name);
formData.append('aliases', aliases); formData.append('aliases', aliases);
const result = await saveAction(formData); const result = (await saveAction(formData)) as { success: boolean };
if (result.success) { if (result.success) {
onChange(); onChange();
@ -43,27 +61,39 @@ function AddCustomEmoji({ close, onChange, ...props }) {
const [clickUpload] = useFileInput(setEmojiPreview, 'emoji'); const [clickUpload] = useFileInput(setEmojiPreview, 'emoji');
const handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.currentTarget.value !== '') {
setErrors((prevState) => ({ ...prevState, name: false }));
}
return setName(e.currentTarget.value);
};
const handleChangeAliases = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.currentTarget.value !== name) {
setErrors((prevState) => ({ ...prevState, aliases: false }));
}
return setAliases(e.currentTarget.value);
};
return ( return (
<VerticalBar.ScrollableContent {...props}> <VerticalBar.ScrollableContent {...props}>
<Field> <Field>
<Field.Label>{t('Name')}</Field.Label> <Field.Label>{t('Name')}</Field.Label>
<Field.Row> <Field.Row>
<TextInput <TextInput value={name} onChange={handleChangeName} placeholder={t('Name')} />
value={name}
onChange={(e) => setName(e.currentTarget.value)}
placeholder={t('Name')}
/>
</Field.Row> </Field.Row>
{errors.name && (
<Field.Error>{t('error-the-field-is-required', { field: t('Name') })}</Field.Error>
)}
</Field> </Field>
<Field> <Field>
<Field.Label>{t('Aliases')}</Field.Label> <Field.Label>{t('Aliases')}</Field.Label>
<Field.Row> <Field.Row>
<TextInput <TextInput value={aliases} onChange={handleChangeAliases} placeholder={t('Aliases')} />
value={aliases}
onChange={(e) => setAliases(e.currentTarget.value)}
placeholder={t('Aliases')}
/>
</Field.Row> </Field.Row>
{errors.aliases && <Field.Error>{t('Custom_Emoji_Error_Same_Name_And_Alias')}</Field.Error>}
</Field> </Field>
<Field> <Field>
<Field.Label <Field.Label
@ -77,6 +107,11 @@ function AddCustomEmoji({ close, onChange, ...props }) {
<Icon name='upload' size='x20' /> <Icon name='upload' size='x20' />
</Button> </Button>
</Field.Label> </Field.Label>
{errors.emoji && (
<Field.Error>
{t('error-the-field-is-required', { field: t('Custom_Emoji') })}
</Field.Error>
)}
{newEmojiPreview && ( {newEmojiPreview && (
<Box display='flex' flexDirection='row' mi='neg-x4' justifyContent='center'> <Box display='flex' flexDirection='row' mi='neg-x4' justifyContent='center'>
<Margins inline='x4'> <Margins inline='x4'>
@ -101,18 +136,8 @@ function AddCustomEmoji({ close, onChange, ...props }) {
</ButtonGroup> </ButtonGroup>
</Field.Row> </Field.Row>
</Field> </Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button primary danger>
<Icon name='trash' mie='x4' />
{t('Delete')}
</Button>
</ButtonGroup>
</Field.Row>
</Field>
</VerticalBar.ScrollableContent> </VerticalBar.ScrollableContent>
); );
} };
export default AddCustomEmoji; export default AddCustomEmoji;

@ -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(
() => [
<GenericTable.HeaderCell
key='name'
direction={sort[1]}
active={sort[0] === 'name'}
onClick={onHeaderClick}
sort='name'
w='x200'
>
{t('Name')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='aliases' w='x200'>
{t('Aliases')}
</GenericTable.HeaderCell>,
],
[onHeaderClick, sort, t],
);
const renderRow = (emojis) => {
const { _id, name, aliases } = emojis;
return (
<Table.Row
key={_id}
onKeyDown={onClick(_id, emojis)}
onClick={onClick(_id, emojis)}
tabIndex={0}
role='link'
action
qa-user-id={_id}
>
<Table.Cell fontScale='p3' color='default'>
<Box withTruncatedText>{name}</Box>
</Table.Cell>
<Table.Cell fontScale='p3' color='default'>
<Box withTruncatedText>{aliases}</Box>
</Table.Cell>
</Table.Row>
);
};
return (
<GenericTable
header={header}
renderRow={renderRow}
results={data?.emojis ?? []}
total={data?.total ?? 0}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
/>
);
}
export default CustomEmoji;

@ -1,5 +1,5 @@
import { Button, Icon } from '@rocket.chat/fuselage'; 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 NotAuthorizedPage from '../../../components/NotAuthorizedPage';
import Page from '../../../components/Page'; import Page from '../../../components/Page';
@ -11,25 +11,25 @@ import AddCustomEmoji from './AddCustomEmoji';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import EditCustomEmojiWithData from './EditCustomEmojiWithData'; import EditCustomEmojiWithData from './EditCustomEmojiWithData';
function CustomEmojiRoute() { const CustomEmojiRoute = (): ReactElement => {
const t = useTranslation();
const route = useRoute('emoji-custom'); const route = useRoute('emoji-custom');
const context = useRouteParameter('context'); const context = useRouteParameter('context');
const id = useRouteParameter('id'); const id = useRouteParameter('id');
const canManageEmoji = usePermission('manage-emoji'); const canManageEmoji = usePermission('manage-emoji');
const t = useTranslation(); const handleItemClick = (_id: string) => (): void => {
const handleItemClick = (_id) => () => {
route.push({ route.push({
context: 'edit', context: 'edit',
id: _id, id: _id,
}); });
}; };
const handleNewButtonClick = useCallback(() => { const handleAddEmoji = useCallback(() => {
route.push({ context: 'new' }); route.push({ context: 'new' });
}, [route]); }, [route]);
const handleClose = () => { const handleClose = (): void => {
route.push({}); route.push({});
}; };
@ -47,7 +47,7 @@ function CustomEmojiRoute() {
<Page flexDirection='row'> <Page flexDirection='row'>
<Page name='admin-emoji-custom'> <Page name='admin-emoji-custom'>
<Page.Header title={t('Custom_Emoji')}> <Page.Header title={t('Custom_Emoji')}>
<Button small onClick={handleNewButtonClick} aria-label={t('New')}> <Button small onClick={handleAddEmoji} aria-label={t('New')}>
<Icon name='plus' /> <Icon name='plus' />
</Button> </Button>
</Page.Header> </Page.Header>
@ -62,7 +62,7 @@ function CustomEmojiRoute() {
{context === 'new' && t('Custom_Emoji_Add')} {context === 'new' && t('Custom_Emoji_Add')}
<VerticalBar.Close onClick={handleClose} /> <VerticalBar.Close onClick={handleClose} />
</VerticalBar.Header> </VerticalBar.Header>
{context === 'edit' && ( {context === 'edit' && id && (
<EditCustomEmojiWithData _id={id} close={handleClose} onChange={handleChange} /> <EditCustomEmojiWithData _id={id} close={handleClose} onChange={handleChange} />
)} )}
{context === 'new' && <AddCustomEmoji close={handleClose} onChange={handleChange} />} {context === 'new' && <AddCustomEmoji close={handleClose} onChange={handleChange} />}
@ -70,6 +70,6 @@ function CustomEmojiRoute() {
)} )}
</Page> </Page>
); );
} };
export default CustomEmojiRoute; export default CustomEmojiRoute;

@ -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 React, { useCallback, useState, useMemo, useEffect, FC, ChangeEvent } from 'react';
import GenericModal from '../../../components/GenericModal'; import GenericModal from '../../../components/GenericModal';
@ -27,6 +36,7 @@ const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...p
const dispatchToastMessage = useToastMessageDispatch(); const dispatchToastMessage = useToastMessageDispatch();
const setModal = useSetModal(); const setModal = useSetModal();
const absoluteUrl = useAbsoluteUrl(); const absoluteUrl = useAbsoluteUrl();
const [errors, setErrors] = useState({ name: false, aliases: false });
const { _id, name: previousName, aliases: previousAliases } = data || {}; const { _id, name: previousName, aliases: previousAliases } = data || {};
@ -62,20 +72,29 @@ const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...p
); );
const handleSave = useCallback(async () => { 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; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append('emoji', emojiFile); emojiFile && formData.append('emoji', emojiFile);
formData.append('_id', _id); formData.append('_id', _id);
formData.append('name', name); formData.append('name', name);
formData.append('aliases', aliases); formData.append('aliases', aliases);
const result = (await saveAction(formData)) as { success: boolean }; const result = (await saveAction(formData)) as { success: boolean };
if (result.success) { if (result.success) {
onChange(); onChange();
close();
} }
}, [emojiFile, _id, name, aliases, saveAction, onChange]); }, [emojiFile, _id, name, aliases, saveAction, onChange, close, newEmojiPreview]);
const deleteAction = useEndpointAction( const deleteAction = useEndpointAction(
'POST', 'POST',
@ -84,23 +103,16 @@ const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...p
); );
const handleDeleteButtonClick = useCallback(() => { const handleDeleteButtonClick = useCallback(() => {
const handleClose = (): void => {
setModal(null);
close();
onChange();
};
const handleDelete = async (): Promise<void> => { const handleDelete = async (): Promise<void> => {
try { try {
await deleteAction(); await deleteAction();
setModal(() => ( dispatchToastMessage({ type: 'success', message: t('Custom_Emoji_Has_Been_Deleted') });
<GenericModal variant='success' onClose={handleClose} onConfirm={handleClose}>
{t('Custom_Emoji_Has_Been_Deleted')}
</GenericModal>
));
} catch (error) { } catch (error) {
dispatchToastMessage({ type: 'error', message: error }); dispatchToastMessage({ type: 'error', message: error });
} finally {
onChange(); onChange();
setModal(null);
close();
} }
}; };
@ -119,76 +131,90 @@ const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...p
{t('Custom_Emoji_Delete_Warning')} {t('Custom_Emoji_Delete_Warning')}
</GenericModal> </GenericModal>
)); ));
}, [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 [clickUpload] = useFileInput(setEmojiFile, 'emoji');
const handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.currentTarget.value !== '') {
setErrors((prevState) => ({ ...prevState, name: false }));
}
return setName(e.currentTarget.value);
};
return ( return (
<VerticalBar.ScrollableContent {...(props as any)}> <VerticalBar.ScrollableContent {...(props as any)}>
<Field> <FieldGroup>
<Field.Label>{t('Name')}</Field.Label> <Field>
<Field.Row> <Field.Label>{t('Name')}</Field.Label>
<TextInput <Field.Row>
value={name} <TextInput value={name} onChange={handleChangeName} placeholder={t('Name')} />
onChange={(e: ChangeEvent<HTMLInputElement>): void => setName(e.currentTarget.value)} </Field.Row>
placeholder={t('Name')} {errors.name && (
/> <Field.Error>{t('error-the-field-is-required', { field: t('Name') })}</Field.Error>
</Field.Row> )}
</Field> </Field>
<Field> <Field>
<Field.Label>{t('Aliases')}</Field.Label> <Field.Label>{t('Aliases')}</Field.Label>
<Field.Row> <Field.Row>
<TextInput value={aliases} onChange={handleAliasesChange} placeholder={t('Aliases')} /> <TextInput value={aliases} onChange={handleChangeAliases} placeholder={t('Aliases')} />
</Field.Row> </Field.Row>
</Field> {errors.aliases && (
<Field> <Field.Error>{t('Custom_Emoji_Error_Same_Name_And_Alias')}</Field.Error>
<Field.Label )}
alignSelf='stretch' </Field>
display='flex' <Field>
justifyContent='space-between' <Field.Label
alignItems='center' alignSelf='stretch'
> display='flex'
{t('Custom_Emoji')} justifyContent='space-between'
<Button square onClick={clickUpload}> alignItems='center'
<Icon name='upload' size='x20' /> >
</Button> {t('Custom_Emoji')}
</Field.Label> <Button square onClick={clickUpload}>
{newEmojiPreview && ( <Icon name='upload' size='x20' />
<Box display='flex' flexDirection='row' mbs='none' justifyContent='center'>
<Margins inline='x4'>
<Box
is='img'
style={{ objectFit: 'contain' }}
w='x120'
h='x120'
src={newEmojiPreview}
/>
</Margins>
</Box>
)}
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} disabled={!hasUnsavedChanges}>
{t('Save')}
</Button>
</ButtonGroup>
</Field.Row>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button primary danger onClick={handleDeleteButtonClick}>
<Icon name='trash' mie='x4' />
{t('Delete')}
</Button> </Button>
</ButtonGroup> </Field.Label>
</Field.Row> {newEmojiPreview && (
</Field> <Box display='flex' flexDirection='row' mbs='none' justifyContent='center'>
<Margins inline='x4'>
<Box
is='img'
style={{ objectFit: 'contain' }}
w='x120'
h='x120'
src={newEmojiPreview}
/>
</Margins>
</Box>
)}
</Field>
</FieldGroup>
<ButtonGroup stretch w='full'>
<Button onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} disabled={!hasUnsavedChanges}>
{t('Save')}
</Button>
</ButtonGroup>
<ButtonGroup stretch w='full'>
<Button primary danger onClick={handleDeleteButtonClick}>
<Icon name='trash' mie='x4' />
{t('Delete')}
</Button>
</ButtonGroup>
</VerticalBar.ScrollableContent> </VerticalBar.ScrollableContent>
); );
}; };

@ -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 React, { useMemo, FC } from 'react';
import { useTranslation } from '../../../contexts/TranslationContext'; import { useTranslation } from '../../../contexts/TranslationContext';
@ -12,7 +20,12 @@ type EditCustomEmojiWithDataProps = {
onChange: () => void; onChange: () => void;
}; };
const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, onChange, ...props }) => { const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({
_id,
onChange,
close,
...props
}) => {
const t = useTranslation(); const t = useTranslation();
const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]); const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]);
@ -52,11 +65,7 @@ const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, onChan
} }
if (error || !data || !data.emojis || data.emojis.update.length < 1) { if (error || !data || !data.emojis || data.emojis.update.length < 1) {
return ( return <Callout title={t('Custom_Emoji_Error_Invalid_Emoji')} type='danger' />;
<Box fontScale='h2' pb='x20'>
{t('Custom_User_Status_Error_Invalid_User_Status')}
</Box>
);
} }
const handleChange = (): void => { const handleChange = (): void => {
@ -64,7 +73,14 @@ const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, onChan
reload && reload(); reload && reload();
}; };
return <EditCustomEmoji data={data.emojis.update[0]} onChange={handleChange} {...props} />; return (
<EditCustomEmoji
data={data.emojis.update[0]}
close={close}
onChange={handleChange}
{...props}
/>
);
}; };
export default EditCustomEmojiWithData; export default EditCustomEmojiWithData;

@ -1296,6 +1296,7 @@
"Custom_Emoji_Delete_Warning": "Deleting an emoji cannot be undone.", "Custom_Emoji_Delete_Warning": "Deleting an emoji cannot be undone.",
"Custom_Emoji_Error_Invalid_Emoji": "Invalid emoji", "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_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_Has_Been_Deleted": "The custom emoji has been deleted.",
"Custom_Emoji_Info": "Custom Emoji Info", "Custom_Emoji_Info": "Custom Emoji Info",
"Custom_Emoji_Updated_Successfully": "Custom emoji updated successfully", "Custom_Emoji_Updated_Successfully": "Custom emoji updated successfully",

Loading…
Cancel
Save