Chore: Convert email inbox feature to TypeScript (#25298)
* chore: email-inbox rest types * Fix email inbox types * Email inbox endpoints to TS * Fix email-inbox params * Chore: Lib emailInbox to ts * chore: migrate to typescript * chore: fix lint * chore: fix table sort * Fix endpoint types * Chore: methods to insert and update was splitted * Removed the wrong validation * Back to default types for sort and query * Purge build files before compile * Require some fields of `IEmailInbox` * Fix weird destructuring Co-authored-by: albuquerquefabio <albuquerquefabio@icloud.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>pull/25468/head
parent
c06874392b
commit
ee1d14618f
@ -1,79 +0,0 @@ |
||||
import { EmailInbox } from '../../../models/server/raw'; |
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
import { Users } from '../../../models'; |
||||
|
||||
export async function findEmailInboxes({ userId, query = {}, pagination: { offset, count, sort } }) { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
const cursor = EmailInbox.find(query, { |
||||
sort: sort || { name: 1 }, |
||||
skip: offset, |
||||
limit: count, |
||||
}); |
||||
|
||||
const total = await cursor.count(); |
||||
|
||||
const emailInboxes = await cursor.toArray(); |
||||
|
||||
return { |
||||
emailInboxes, |
||||
count: emailInboxes.length, |
||||
offset, |
||||
total, |
||||
}; |
||||
} |
||||
|
||||
export async function findOneEmailInbox({ userId, _id }) { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOneById(_id); |
||||
} |
||||
|
||||
export async function insertOneOrUpdateEmailInbox(userId, emailInboxParams) { |
||||
const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; |
||||
|
||||
if (!_id) { |
||||
emailInboxParams._createdAt = new Date(); |
||||
emailInboxParams._updatedAt = new Date(); |
||||
emailInboxParams._createdBy = Users.findOne(userId, { fields: { username: 1 } }); |
||||
return EmailInbox.insertOne(emailInboxParams); |
||||
} |
||||
|
||||
const emailInbox = await findOneEmailInbox({ userId, id: _id }); |
||||
|
||||
if (!emailInbox) { |
||||
throw new Error('error-invalid-email-inbox'); |
||||
} |
||||
|
||||
const updateEmailInbox = { |
||||
$set: { |
||||
active, |
||||
name, |
||||
email, |
||||
description, |
||||
senderInfo, |
||||
smtp, |
||||
imap, |
||||
_updatedAt: new Date(), |
||||
}, |
||||
}; |
||||
|
||||
if (department === 'All') { |
||||
updateEmailInbox.$unset = { |
||||
department: 1, |
||||
}; |
||||
} else { |
||||
updateEmailInbox.$set.department = department; |
||||
} |
||||
|
||||
return EmailInbox.updateOne({ _id }, updateEmailInbox); |
||||
} |
||||
|
||||
export async function findOneEmailInboxByEmail({ userId, email }) { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOne({ email }); |
||||
} |
||||
@ -0,0 +1,101 @@ |
||||
import { IEmailInbox } from '@rocket.chat/core-typings'; |
||||
import { InsertOneWriteOpResult, UpdateWriteOpResult, WithId } from 'mongodb'; |
||||
|
||||
import { EmailInbox } from '../../../models/server/raw'; |
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
import { Users } from '../../../models/server'; |
||||
|
||||
export const findEmailInboxes = async ({ |
||||
userId, |
||||
query = {}, |
||||
pagination: { offset, count, sort }, |
||||
}: { |
||||
userId: string; |
||||
query?: {}; |
||||
pagination: { |
||||
offset: number; |
||||
count: number; |
||||
sort?: {}; |
||||
}; |
||||
}): Promise<{ |
||||
emailInboxes: IEmailInbox[]; |
||||
total: number; |
||||
count: number; |
||||
offset: number; |
||||
}> => { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
const cursor = EmailInbox.find(query, { |
||||
sort: sort || { name: 1 }, |
||||
skip: offset, |
||||
limit: count, |
||||
}); |
||||
|
||||
const total = await cursor.count(); |
||||
|
||||
const emailInboxes = await cursor.toArray(); |
||||
|
||||
return { |
||||
emailInboxes, |
||||
count: emailInboxes.length, |
||||
offset, |
||||
total, |
||||
}; |
||||
}; |
||||
|
||||
export const findOneEmailInbox = async ({ userId, _id }: { userId: string; _id: string }): Promise<IEmailInbox | null> => { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOneById(_id); |
||||
}; |
||||
export const insertOneEmailInbox = async ( |
||||
userId: string, |
||||
emailInboxParams: Pick<IEmailInbox, 'active' | 'name' | 'email' | 'description' | 'senderInfo' | 'department' | 'smtp' | 'imap'>, |
||||
): Promise<InsertOneWriteOpResult<WithId<IEmailInbox>>> => { |
||||
const obj = { |
||||
...emailInboxParams, |
||||
_createdAt: new Date(), |
||||
_updatedAt: new Date(), |
||||
_createdBy: Users.findOne(userId, { fields: { username: 1 } }), |
||||
}; |
||||
return EmailInbox.insertOne(obj); |
||||
}; |
||||
|
||||
export const updateEmailInbox = async ( |
||||
userId: string, |
||||
emailInboxParams: Pick<IEmailInbox, '_id' | 'active' | 'name' | 'email' | 'description' | 'senderInfo' | 'department' | 'smtp' | 'imap'>, |
||||
): Promise<InsertOneWriteOpResult<WithId<IEmailInbox>> | UpdateWriteOpResult> => { |
||||
const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; |
||||
|
||||
const emailInbox = await findOneEmailInbox({ userId, _id }); |
||||
|
||||
if (!emailInbox) { |
||||
throw new Error('error-invalid-email-inbox'); |
||||
} |
||||
|
||||
const updateEmailInbox = { |
||||
$set: { |
||||
active, |
||||
name, |
||||
email, |
||||
description, |
||||
senderInfo, |
||||
smtp, |
||||
imap, |
||||
_updatedAt: new Date(), |
||||
...(department !== 'All' && { department }), |
||||
}, |
||||
...(department === 'All' && { $unset: { department: 1 as const } }), |
||||
}; |
||||
|
||||
return EmailInbox.updateOne({ _id }, updateEmailInbox); |
||||
}; |
||||
|
||||
export const findOneEmailInboxByEmail = async ({ userId, email }: { userId: string; email: string }): Promise<IEmailInbox | null> => { |
||||
if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOne({ email }); |
||||
}; |
||||
@ -1,82 +0,0 @@ |
||||
import { Table } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo, useCallback, useState } from 'react'; |
||||
|
||||
import GenericTable from '../../../components/GenericTable'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import SendTestButton from './SendTestButton'; |
||||
|
||||
const useQuery = ({ itemsPerPage, current }, [column, direction]) => |
||||
useMemo( |
||||
() => ({ |
||||
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), |
||||
...(itemsPerPage && { count: itemsPerPage }), |
||||
...(current && { offset: current }), |
||||
}), |
||||
[column, current, direction, itemsPerPage], |
||||
); |
||||
|
||||
function EmailInboxTable() { |
||||
const t = useTranslation(); |
||||
|
||||
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); |
||||
const [sort] = useState(['name', 'asc']); |
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
const query = useQuery(debouncedParams, debouncedSort); |
||||
const router = useRoute('admin-email-inboxes'); |
||||
|
||||
const onClick = useCallback( |
||||
(_id) => () => |
||||
router.push({ |
||||
context: 'edit', |
||||
_id, |
||||
}), |
||||
[router], |
||||
); |
||||
|
||||
const header = useMemo( |
||||
() => |
||||
[ |
||||
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'}> |
||||
{t('Name')} |
||||
</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'email'} direction={sort[1]} active={sort[0] === 'email'}> |
||||
{t('Email')} |
||||
</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'active'} direction={sort[1]} active={sort[0] === 'active'}> |
||||
{t('Active')} |
||||
</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'sendTest'} w='x60'></GenericTable.HeaderCell>, |
||||
].filter(Boolean), |
||||
[sort, t], |
||||
); |
||||
|
||||
const { value: data } = useEndpointData('email-inbox.list', query); |
||||
|
||||
const renderRow = useCallback( |
||||
({ _id, name, email, active }) => ( |
||||
<Table.Row action key={_id} onKeyDown={onClick(_id)} onClick={onClick(_id)} tabIndex={0} role='link' qa-room-id={_id}> |
||||
<Table.Cell withTruncatedText>{name}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{email}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{active ? t('Yes') : t('No')}</Table.Cell> |
||||
<SendTestButton id={_id} /> |
||||
</Table.Row> |
||||
), |
||||
[onClick, t], |
||||
); |
||||
|
||||
return ( |
||||
<GenericTable |
||||
header={header} |
||||
renderRow={renderRow} |
||||
results={data && data.emailInboxes} |
||||
total={data && data.total} |
||||
setParams={setParams} |
||||
params={params} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
export default EmailInboxTable; |
||||
@ -0,0 +1,128 @@ |
||||
import { Box, Pagination } from '@rocket.chat/fuselage'; |
||||
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo, useCallback, ReactElement } from 'react'; |
||||
|
||||
import { |
||||
GenericTable, |
||||
GenericTableBody, |
||||
GenericTableCell, |
||||
GenericTableHeader, |
||||
GenericTableHeaderCell, |
||||
GenericTableLoadingTable, |
||||
GenericTableRow, |
||||
} from '../../../components/GenericTable'; |
||||
import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; |
||||
import { useSort } from '../../../components/GenericTable/hooks/useSort'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase'; |
||||
import SendTestButton from './SendTestButton'; |
||||
|
||||
const useQuery = ( |
||||
{ |
||||
itemsPerPage, |
||||
current, |
||||
}: { |
||||
itemsPerPage: number; |
||||
current: number; |
||||
}, |
||||
[column, direction]: string[], |
||||
): { |
||||
offset?: number; |
||||
count?: number; |
||||
sort: string; |
||||
} => |
||||
useMemo( |
||||
() => ({ |
||||
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), |
||||
...(itemsPerPage && { count: itemsPerPage }), |
||||
...(current && { offset: current }), |
||||
}), |
||||
[column, current, direction, itemsPerPage], |
||||
); |
||||
|
||||
const EmailInboxTable = (): ReactElement => { |
||||
const t = useTranslation(); |
||||
|
||||
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); |
||||
const { sortBy, sortDirection, setSort } = useSort<'name'>('name'); |
||||
const query = useQuery({ itemsPerPage, current }, [sortBy, sortDirection]); |
||||
|
||||
const router = useRoute('admin-email-inboxes'); |
||||
|
||||
const onClick = useCallback( |
||||
(_id) => (): void => { |
||||
router.push({ |
||||
context: 'edit', |
||||
_id, |
||||
}); |
||||
}, |
||||
[router], |
||||
); |
||||
|
||||
const { phase, value: { emailInboxes = [], count = 0 } = {} } = useEndpointData('email-inbox.list', query); |
||||
|
||||
return ( |
||||
<> |
||||
<GenericTable> |
||||
<GenericTableHeader> |
||||
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name' w='x200'> |
||||
{t('Name')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='email' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name' w='x200'> |
||||
{t('Email')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='active' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name' w='x200'> |
||||
{t('Active')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell |
||||
key='sendTest' |
||||
direction={sortDirection} |
||||
active={sortBy === 'name'} |
||||
onClick={setSort} |
||||
sort='name' |
||||
w='x200' |
||||
></GenericTableHeaderCell> |
||||
</GenericTableHeader> |
||||
<GenericTableBody> |
||||
{phase === AsyncStatePhase.LOADING && <GenericTableLoadingTable headerCells={2} />} |
||||
{phase === AsyncStatePhase.RESOLVED && |
||||
count > 0 && |
||||
emailInboxes.map((emailInbox) => ( |
||||
<GenericTableRow |
||||
key={emailInbox._id} |
||||
onKeyDown={onClick(emailInbox._id)} |
||||
onClick={onClick(emailInbox._id)} |
||||
tabIndex={0} |
||||
role='link' |
||||
action |
||||
width='full' |
||||
> |
||||
<GenericTableCell fontScale='p1' color='default'> |
||||
<Box withTruncatedText>{emailInbox.name}</Box> |
||||
</GenericTableCell> |
||||
<GenericTableCell fontScale='p1' color='default'> |
||||
<Box withTruncatedText>{emailInbox.email}</Box> |
||||
</GenericTableCell> |
||||
<GenericTableCell fontScale='p1' color='default'> |
||||
<Box withTruncatedText>{emailInbox.active ? t('Yes') : t('No')}</Box> |
||||
</GenericTableCell> |
||||
<SendTestButton id={emailInbox._id} /> |
||||
</GenericTableRow> |
||||
))} |
||||
</GenericTableBody> |
||||
</GenericTable> |
||||
{phase === AsyncStatePhase.RESOLVED && ( |
||||
<Pagination |
||||
current={current} |
||||
itemsPerPage={itemsPerPage} |
||||
count={count} |
||||
onSetItemsPerPage={onSetItemsPerPage} |
||||
onSetCurrent={onSetCurrent} |
||||
{...paginationProps} |
||||
/> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default EmailInboxTable; |
||||
@ -1,27 +0,0 @@ |
||||
import { Button, Table, Icon } from '@rocket.chat/fuselage'; |
||||
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
function SendTestButton({ id }) { |
||||
const t = useTranslation(); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const sendTest = useEndpoint('POST', `email-inbox.send-test/${id}`); |
||||
|
||||
return ( |
||||
<Table.Cell fontScale='p2' color='hint' withTruncatedText> |
||||
<Button |
||||
small |
||||
ghost |
||||
title={t('Send_Test_Email')} |
||||
onClick={(e) => |
||||
e.preventDefault() & e.stopPropagation() & sendTest() & dispatchToastMessage({ type: 'success', message: t('Email_sent') }) |
||||
} |
||||
> |
||||
<Icon name='send' size='x20' /> |
||||
</Button> |
||||
</Table.Cell> |
||||
); |
||||
} |
||||
|
||||
export default SendTestButton; |
||||
@ -0,0 +1,34 @@ |
||||
import { Button, Icon, TableCell } from '@rocket.chat/fuselage'; |
||||
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { ReactElement } from 'react'; |
||||
|
||||
type SendTestButtonProps = { |
||||
id: string; |
||||
}; |
||||
|
||||
const SendTestButton = ({ id }: SendTestButtonProps): ReactElement => { |
||||
const t = useTranslation(); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const sendTest = useEndpoint('POST', `email-inbox.send-test/${id}`); |
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLElement, MouseEvent>): void => { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
sendTest(); |
||||
dispatchToastMessage({ |
||||
type: 'success', |
||||
message: t('Email_sent'), |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<TableCell fontScale='p2' color='hint' withTruncatedText> |
||||
<Button small ghost title={t('Send_Test_Email')} onClick={handleOnClick}> |
||||
<Icon name='send' size='x20' /> |
||||
</Button> |
||||
</TableCell> |
||||
); |
||||
}; |
||||
|
||||
export default SendTestButton; |
||||
@ -1,8 +1,8 @@ |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
import React, { ReactElement } from 'react'; |
||||
|
||||
export const FormSkeleton = (props) => ( |
||||
<Box w='full' pb='x24' {...props}> |
||||
export const FormSkeleton = (): ReactElement => ( |
||||
<Box w='full' pb='x24'> |
||||
<Skeleton mbe='x8' /> |
||||
<Skeleton mbe='x4' /> |
||||
<Skeleton mbe='x4' /> |
||||
@ -0,0 +1,45 @@ |
||||
import type { IEmailInbox } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { PaginatedRequest } from '../helpers/PaginatedRequest'; |
||||
import type { PaginatedResult } from '../helpers/PaginatedResult'; |
||||
|
||||
export type EmailInboxEndpoints = { |
||||
'email-inbox.list': { |
||||
GET: (params: PaginatedRequest<{ query?: string }>) => PaginatedResult<{ emailInboxes: IEmailInbox[] }>; |
||||
}; |
||||
'email-inbox': { |
||||
POST: (params: { |
||||
_id?: string; |
||||
name: string; |
||||
email: string; |
||||
active: boolean; |
||||
description: string; |
||||
senderInfo: string; |
||||
department: string; |
||||
smtp: { |
||||
password: string; |
||||
port: number; |
||||
secure: boolean; |
||||
server: string; |
||||
username: string; |
||||
}; |
||||
imap: { |
||||
password: string; |
||||
port: number; |
||||
secure: boolean; |
||||
server: string; |
||||
username: string; |
||||
}; |
||||
}) => { _id: string }; |
||||
}; |
||||
'email-inbox/:_id': { |
||||
GET: (params: void) => IEmailInbox | null; |
||||
DELETE: (params: void) => { _id: string }; |
||||
}; |
||||
'email-inbox.search': { |
||||
GET: (params: { email: string }) => { emailInbox: IEmailInbox | null }; |
||||
}; |
||||
'email-inbox.send-test/:_id': { |
||||
POST: (params: void) => { _id: string }; |
||||
}; |
||||
}; |
||||
Loading…
Reference in new issue