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

pull/23948/head
Sidharth Mohanty 3 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 React, { useState, FC } from 'react';
import React, { useState, ReactElement, ComponentProps } from 'react';
import PageContext from './PageContext';
const Page: FC = (props) => {
const Page = (props: ComponentProps<typeof Box>): ReactElement => {
const [border, setBorder] = useState(false);
return (
<PageContext.Provider value={[border, setBorder]}>

@ -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<Blob>();
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<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 (
<VerticalBar.ScrollableContent {...props}>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput
value={name}
onChange={(e) => setName(e.currentTarget.value)}
placeholder={t('Name')}
/>
<TextInput value={name} onChange={handleChangeName} placeholder={t('Name')} />
</Field.Row>
{errors.name && (
<Field.Error>{t('error-the-field-is-required', { field: t('Name') })}</Field.Error>
)}
</Field>
<Field>
<Field.Label>{t('Aliases')}</Field.Label>
<Field.Row>
<TextInput
value={aliases}
onChange={(e) => setAliases(e.currentTarget.value)}
placeholder={t('Aliases')}
/>
<TextInput value={aliases} onChange={handleChangeAliases} placeholder={t('Aliases')} />
</Field.Row>
{errors.aliases && <Field.Error>{t('Custom_Emoji_Error_Same_Name_And_Alias')}</Field.Error>}
</Field>
<Field>
<Field.Label
@ -77,6 +107,11 @@ function AddCustomEmoji({ close, onChange, ...props }) {
<Icon name='upload' size='x20' />
</Button>
</Field.Label>
{errors.emoji && (
<Field.Error>
{t('error-the-field-is-required', { field: t('Custom_Emoji') })}
</Field.Error>
)}
{newEmojiPreview && (
<Box display='flex' flexDirection='row' mi='neg-x4' justifyContent='center'>
<Margins inline='x4'>
@ -101,18 +136,8 @@ function AddCustomEmoji({ close, onChange, ...props }) {
</ButtonGroup>
</Field.Row>
</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>
);
}
};
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 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() {
<Page flexDirection='row'>
<Page name='admin-emoji-custom'>
<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' />
</Button>
</Page.Header>
@ -62,7 +62,7 @@ function CustomEmojiRoute() {
{context === 'new' && t('Custom_Emoji_Add')}
<VerticalBar.Close onClick={handleClose} />
</VerticalBar.Header>
{context === 'edit' && (
{context === 'edit' && id && (
<EditCustomEmojiWithData _id={id} close={handleClose} onChange={handleChange} />
)}
{context === 'new' && <AddCustomEmoji close={handleClose} onChange={handleChange} />}
@ -70,6 +70,6 @@ function CustomEmojiRoute() {
)}
</Page>
);
}
};
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 GenericModal from '../../../components/GenericModal';
@ -27,6 +36,7 @@ const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ 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<EditCustomEmojiProps> = ({ 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<EditCustomEmojiProps> = ({ close, onChange, data, ...p
);
const handleDeleteButtonClick = useCallback(() => {
const handleClose = (): void => {
setModal(null);
close();
onChange();
};
const handleDelete = async (): Promise<void> => {
try {
await deleteAction();
setModal(() => (
<GenericModal variant='success' onClose={handleClose} onConfirm={handleClose}>
{t('Custom_Emoji_Has_Been_Deleted')}
</GenericModal>
));
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<EditCustomEmojiProps> = ({ close, onChange, data, ...p
{t('Custom_Emoji_Delete_Warning')}
</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 handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => {
if (e.currentTarget.value !== '') {
setErrors((prevState) => ({ ...prevState, name: false }));
}
return setName(e.currentTarget.value);
};
return (
<VerticalBar.ScrollableContent {...(props as any)}>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput
value={name}
onChange={(e: ChangeEvent<HTMLInputElement>): void => setName(e.currentTarget.value)}
placeholder={t('Name')}
/>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Aliases')}</Field.Label>
<Field.Row>
<TextInput value={aliases} onChange={handleAliasesChange} placeholder={t('Aliases')} />
</Field.Row>
</Field>
<Field>
<Field.Label
alignSelf='stretch'
display='flex'
justifyContent='space-between'
alignItems='center'
>
{t('Custom_Emoji')}
<Button square onClick={clickUpload}>
<Icon name='upload' size='x20' />
</Button>
</Field.Label>
{newEmojiPreview && (
<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')}
<FieldGroup>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput value={name} onChange={handleChangeName} placeholder={t('Name')} />
</Field.Row>
{errors.name && (
<Field.Error>{t('error-the-field-is-required', { field: t('Name') })}</Field.Error>
)}
</Field>
<Field>
<Field.Label>{t('Aliases')}</Field.Label>
<Field.Row>
<TextInput value={aliases} onChange={handleChangeAliases} placeholder={t('Aliases')} />
</Field.Row>
{errors.aliases && (
<Field.Error>{t('Custom_Emoji_Error_Same_Name_And_Alias')}</Field.Error>
)}
</Field>
<Field>
<Field.Label
alignSelf='stretch'
display='flex'
justifyContent='space-between'
alignItems='center'
>
{t('Custom_Emoji')}
<Button square onClick={clickUpload}>
<Icon name='upload' size='x20' />
</Button>
</ButtonGroup>
</Field.Row>
</Field>
</Field.Label>
{newEmojiPreview && (
<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>
);
};

@ -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<EditCustomEmojiWithDataProps> = ({ _id, onChange, ...props }) => {
const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({
_id,
onChange,
close,
...props
}) => {
const t = useTranslation();
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) {
return (
<Box fontScale='h2' pb='x20'>
{t('Custom_User_Status_Error_Invalid_User_Status')}
</Box>
);
return <Callout title={t('Custom_Emoji_Error_Invalid_Emoji')} type='danger' />;
}
const handleChange = (): void => {
@ -64,7 +73,14 @@ const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, onChan
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;

@ -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",

Loading…
Cancel
Save