[IMPROVE] Rewrite remove room invite modal #23781
parent
a0ed0b1e6c
commit
8b90ef8f4b
@ -0,0 +1,5 @@ |
||||
import moment from 'moment'; |
||||
import { useCallback } from 'react'; |
||||
|
||||
export const useTimeFromNow = (withSuffix: boolean): ((date: Date) => string) => |
||||
useCallback((date) => moment(date).fromNow(!withSuffix), [withSuffix]); |
||||
@ -1,100 +0,0 @@ |
||||
import { Button, Icon, Table, Box } from '@rocket.chat/fuselage'; |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import moment from 'moment'; |
||||
import React from 'react'; |
||||
|
||||
import { useModal } from '../../../contexts/ModalContext'; |
||||
import { useEndpoint } from '../../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; |
||||
|
||||
function InviteRow({ _id, createdAt, expires, days, uses, maxUses, onRemove }) { |
||||
const t = useTranslation(); |
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
const modal = useModal(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const removeInvite = useEndpoint('DELETE', `removeInvite/${_id}`); |
||||
|
||||
const daysToExpire = ({ expires, days }) => { |
||||
if (days > 0) { |
||||
if (expires < Date.now()) { |
||||
return t('Expired'); |
||||
} |
||||
|
||||
return moment(expires).fromNow(true); |
||||
} |
||||
|
||||
return t('Never'); |
||||
}; |
||||
|
||||
const maxUsesLeft = ({ maxUses, uses }) => { |
||||
if (maxUses > 0) { |
||||
if (uses >= maxUses) { |
||||
return 0; |
||||
} |
||||
|
||||
return maxUses - uses; |
||||
} |
||||
|
||||
return t('Unlimited'); |
||||
}; |
||||
|
||||
const handleRemoveButtonClick = async (event) => { |
||||
event.stopPropagation(); |
||||
|
||||
modal.open( |
||||
{ |
||||
// TODO REFACTOR
|
||||
text: t('Are_you_sure_you_want_to_delete_this_record'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, |
||||
async (confirmed) => { |
||||
if (!confirmed) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
await removeInvite(); |
||||
onRemove && onRemove(_id); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, |
||||
); |
||||
}; |
||||
|
||||
const notSmall = useMediaQuery('(min-width: 768px)'); |
||||
|
||||
return ( |
||||
<Table.Row> |
||||
<Table.Cell> |
||||
<Box color='hint' fontScale='p3'> |
||||
{_id} |
||||
</Box> |
||||
</Table.Cell> |
||||
{notSmall && ( |
||||
<> |
||||
<Table.Cell>{formatDateAndTime(createdAt)}</Table.Cell> |
||||
<Table.Cell>{daysToExpire({ expires, days })}</Table.Cell> |
||||
<Table.Cell>{uses}</Table.Cell> |
||||
<Table.Cell>{maxUsesLeft({ maxUses, uses })}</Table.Cell> |
||||
</> |
||||
)} |
||||
<Table.Cell> |
||||
<Button ghost danger small square onClick={handleRemoveButtonClick}> |
||||
<Icon name='cross' size='x20' /> |
||||
</Button> |
||||
</Table.Cell> |
||||
</Table.Row> |
||||
); |
||||
} |
||||
|
||||
export default InviteRow; |
||||
@ -0,0 +1,96 @@ |
||||
import { Button, Icon, Box } from '@rocket.chat/fuselage'; |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { ReactElement, MouseEvent } from 'react'; |
||||
|
||||
import { IInvite } from '../../../../definition/IInvite'; |
||||
import { GenericTableCell, GenericTableRow } from '../../../components/GenericTable'; |
||||
import { useEndpoint } from '../../../contexts/ServerContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; |
||||
import { useTimeFromNow } from '../../../hooks/useTimeFromNow'; |
||||
|
||||
const isExpired = (expires: IInvite['expires']): boolean => { |
||||
if (expires && expires.getTime() < new Date().getTime()) { |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
}; |
||||
|
||||
type InviteRowProps = Omit<IInvite, 'createdAt' | 'expires' | '_updatedAt'> & { |
||||
onRemove: (removeInvite: () => void) => void; |
||||
_updatedAt: string; |
||||
createdAt: string; |
||||
expires: string | null; |
||||
}; |
||||
|
||||
const InviteRow = ({ |
||||
_id, |
||||
createdAt, |
||||
expires, |
||||
uses, |
||||
maxUses, |
||||
onRemove, |
||||
}: InviteRowProps): ReactElement => { |
||||
const t = useTranslation(); |
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
const removeInvite = useEndpoint('DELETE', `removeInvite/${_id}`); |
||||
|
||||
const getTimeFromNow = useTimeFromNow(false); |
||||
|
||||
const daysToExpire = (expires: IInvite['expires']): string => { |
||||
if (expires) { |
||||
if (isExpired(expires)) { |
||||
return t('Expired'); |
||||
} |
||||
|
||||
return getTimeFromNow(expires); |
||||
} |
||||
|
||||
return t('Never'); |
||||
}; |
||||
|
||||
const maxUsesLeft = (maxUses: IInvite['maxUses'], uses: IInvite['uses']): number | string => { |
||||
if (maxUses > 0) { |
||||
if (uses >= maxUses) { |
||||
return 0; |
||||
} |
||||
|
||||
return maxUses - uses; |
||||
} |
||||
|
||||
return t('Unlimited'); |
||||
}; |
||||
|
||||
const handleRemoveButtonClick = async (event: MouseEvent<HTMLElement>): Promise<void> => { |
||||
event.stopPropagation(); |
||||
onRemove(removeInvite); |
||||
}; |
||||
|
||||
const notSmall = useMediaQuery('(min-width: 768px)'); |
||||
|
||||
return ( |
||||
<GenericTableRow> |
||||
<GenericTableCell> |
||||
<Box color='hint' fontScale='p3'> |
||||
{_id} |
||||
</Box> |
||||
</GenericTableCell> |
||||
{notSmall && ( |
||||
<> |
||||
<GenericTableCell>{formatDateAndTime(new Date(createdAt))}</GenericTableCell> |
||||
<GenericTableCell>{daysToExpire(expires ? new Date(expires) : null)}</GenericTableCell> |
||||
<GenericTableCell>{uses}</GenericTableCell> |
||||
<GenericTableCell>{maxUsesLeft(maxUses, uses)}</GenericTableCell> |
||||
</> |
||||
)} |
||||
<GenericTableCell> |
||||
<Button ghost danger small square onClick={handleRemoveButtonClick}> |
||||
<Icon name='cross' size='x20' /> |
||||
</Button> |
||||
</GenericTableCell> |
||||
</GenericTableRow> |
||||
); |
||||
}; |
||||
|
||||
export default InviteRow; |
||||
@ -1,79 +0,0 @@ |
||||
import { Table } from '@rocket.chat/fuselage'; |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import GenericTable from '../../../components/GenericTable'; |
||||
import Page from '../../../components/Page'; |
||||
import { useEndpoint } from '../../../contexts/ServerContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import InviteRow from './InviteRow'; |
||||
|
||||
function InvitesPage() { |
||||
const t = useTranslation(); |
||||
|
||||
const [invites, setInvites] = useState([]); |
||||
|
||||
const listInvites = useEndpoint('GET', 'listInvites'); |
||||
|
||||
useEffect(() => { |
||||
const loadInvites = async () => { |
||||
const result = (await listInvites()) || []; |
||||
|
||||
const invites = result.map((data) => ({ |
||||
...data, |
||||
createdAt: new Date(data.createdAt), |
||||
expires: data.expires ? new Date(data.expires) : '', |
||||
})); |
||||
|
||||
setInvites(invites); |
||||
}; |
||||
|
||||
loadInvites(); |
||||
}, [listInvites]); |
||||
|
||||
const handleInviteRemove = (_id) => { |
||||
setInvites((invites = []) => invites.filter((invite) => invite._id !== _id)); |
||||
}; |
||||
|
||||
const notSmall = useMediaQuery('(min-width: 768px)'); |
||||
|
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Invites')} /> |
||||
<Page.Content> |
||||
<GenericTable |
||||
results={invites} |
||||
header={ |
||||
<> |
||||
<Table.Cell is='th' width={notSmall ? '20%' : '80%'}> |
||||
{t('Token')} |
||||
</Table.Cell> |
||||
{notSmall && ( |
||||
<> |
||||
<Table.Cell is='th' width='35%'> |
||||
{t('Created_at')} |
||||
</Table.Cell> |
||||
<Table.Cell is='th' width='20%'> |
||||
{t('Expiration')} |
||||
</Table.Cell> |
||||
<Table.Cell is='th' width='10%'> |
||||
{t('Uses')} |
||||
</Table.Cell> |
||||
<Table.Cell is='th' width='10%'> |
||||
{t('Uses_left')} |
||||
</Table.Cell> |
||||
</> |
||||
)} |
||||
<Table.Cell is='th' /> |
||||
</> |
||||
} |
||||
renderRow={(invite) => ( |
||||
<InviteRow key={invite._id} {...invite} onRemove={handleInviteRemove} /> |
||||
)} |
||||
/> |
||||
</Page.Content> |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
export default InvitesPage; |
||||
@ -0,0 +1,91 @@ |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { ReactElement } from 'react'; |
||||
|
||||
import GenericModal from '../../../components/GenericModal'; |
||||
import { |
||||
GenericTable, |
||||
GenericTableBody, |
||||
GenericTableHeader, |
||||
GenericTableHeaderCell, |
||||
GenericTableLoadingTable, |
||||
} from '../../../components/GenericTable'; |
||||
import Page from '../../../components/Page'; |
||||
import { useSetModal } from '../../../contexts/ModalContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import { AsyncStatePhase } from '../../../lib/asyncState'; |
||||
import InviteRow from './InviteRow'; |
||||
|
||||
const InvitesPage = (): ReactElement => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const setModal = useSetModal(); |
||||
|
||||
const { phase, value, reload } = useEndpointData('listInvites'); |
||||
|
||||
const onRemove = (removeInvite: () => void): void => { |
||||
const confirmRemove = async (): Promise<void> => { |
||||
try { |
||||
await removeInvite(); |
||||
dispatchToastMessage({ type: 'success', message: t('Invite_removed') }); |
||||
reload(); |
||||
} catch (error) { |
||||
if (typeof error === 'string' || error instanceof Error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
} finally { |
||||
setModal(); |
||||
} |
||||
}; |
||||
|
||||
setModal( |
||||
<GenericModal |
||||
title={t('Are_you_sure')} |
||||
children={t('Are_you_sure_you_want_to_delete_this_record')} |
||||
variant='danger' |
||||
confirmText={t('Yes')} |
||||
cancelText={t('No')} |
||||
onClose={(): void => setModal()} |
||||
onCancel={(): void => setModal()} |
||||
onConfirm={confirmRemove} |
||||
/>, |
||||
); |
||||
}; |
||||
|
||||
const notSmall = useMediaQuery('(min-width: 768px)'); |
||||
|
||||
return ( |
||||
<Page> |
||||
<Page.Header title={t('Invites')} /> |
||||
<Page.Content> |
||||
<GenericTable> |
||||
<GenericTableHeader> |
||||
<GenericTableHeaderCell w={notSmall ? '20%' : '80%'}> |
||||
{t('Token')} |
||||
</GenericTableHeaderCell> |
||||
{notSmall && ( |
||||
<> |
||||
<GenericTableHeaderCell w='35%'>{t('Created_at')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='20%'>{t('Expiration')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='10%'>{t('Uses')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='10%'>{t('Uses_left')}</GenericTableHeaderCell> |
||||
</> |
||||
)} |
||||
<GenericTableHeaderCell /> |
||||
</GenericTableHeader> |
||||
<GenericTableBody> |
||||
{phase === AsyncStatePhase.LOADING && ( |
||||
<GenericTableLoadingTable headerCells={notSmall ? 4 : 1} /> |
||||
)} |
||||
{phase === AsyncStatePhase.RESOLVED && |
||||
Array.isArray(value) && |
||||
value.map((invite) => <InviteRow key={invite._id} {...invite} onRemove={onRemove} />)} |
||||
</GenericTableBody> |
||||
</GenericTable> |
||||
</Page.Content> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
export default InvitesPage; |
||||
@ -0,0 +1,10 @@ |
||||
import { IInvite } from '../../IInvite'; |
||||
|
||||
export type InvitesEndpoints = { |
||||
'listInvites': { |
||||
GET: () => Array<IInvite>; |
||||
}; |
||||
'removeInvite/:_id': { |
||||
DELETE: () => void; |
||||
}; |
||||
}; |
||||
Loading…
Reference in new issue