[IMPROVE] Rewrite remove room invite modal #23781

pull/23831/head
Douglas Fabris 4 years ago committed by GitHub
parent a0ed0b1e6c
commit 8b90ef8f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      client/components/GenericModal.tsx
  2. 5
      client/hooks/useTimeFromNow.ts
  3. 100
      client/views/admin/invites/InviteRow.js
  4. 96
      client/views/admin/invites/InviteRow.tsx
  5. 79
      client/views/admin/invites/InvitesPage.js
  6. 91
      client/views/admin/invites/InvitesPage.tsx
  7. 6
      client/views/admin/invites/InvitesRoute.tsx
  8. 2
      definition/IInvite.ts
  9. 4
      definition/rest/index.ts
  10. 10
      definition/rest/v1/invites.ts
  11. 3
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -8,6 +8,7 @@ type VariantType = 'danger' | 'warning' | 'info' | 'success';
type GenericModalProps = RequiredModalProps & {
variant?: VariantType;
children?: ReactNode;
cancelText?: string;
confirmText?: string;
title?: string | ReactElement;

@ -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;

@ -1,10 +1,10 @@
import React from 'react';
import React, { ReactElement } from 'react';
import NotAuthorizedPage from '../../../components/NotAuthorizedPage';
import { usePermission } from '../../../contexts/AuthorizationContext';
import InvitesPage from './InvitesPage';
function InvitesRoute() {
const InvitesRoute = (): ReactElement => {
const canCreateInviteLinks = usePermission('create-invite-links');
if (!canCreateInviteLinks) {
@ -12,6 +12,6 @@ function InvitesRoute() {
}
return <InvitesPage />;
}
};
export default InvitesRoute;

@ -6,6 +6,6 @@ export interface IInvite extends IRocketChatRecord {
rid: string;
userId: string;
createdAt: Date;
expires: Date;
expires: Date | null;
uses: number;
}

@ -13,6 +13,7 @@ import type { EmojiCustomEndpoints } from './v1/emojiCustom';
import type { GroupsEndpoints } from './v1/groups';
import type { ImEndpoints } from './v1/im';
import type { InstancesEndpoints } from './v1/instances';
import type { InvitesEndpoints } from './v1/invites';
import type { LDAPEndpoints } from './v1/ldap';
import type { LicensesEndpoints } from './v1/licenses';
import type { MiscEndpoints } from './v1/misc';
@ -47,7 +48,8 @@ type CommunityEndpoints = BannersEndpoints &
LicensesEndpoints &
MiscEndpoints &
PermissionsEndpoints &
InstancesEndpoints;
InstancesEndpoints &
InvitesEndpoints;
type Endpoints = CommunityEndpoints & EnterpriseEndpoints;

@ -0,0 +1,10 @@
import { IInvite } from '../../IInvite';
export type InvitesEndpoints = {
'listInvites': {
GET: () => Array<IInvite>;
};
'removeInvite/:_id': {
DELETE: () => void;
};
};

@ -1818,6 +1818,7 @@
"Exit_Full_Screen": "Exit Full Screen",
"Expand": "Expand",
"Experimental_Feature_Alert": "This is an experimental feature! Please be aware that it may change, break, or even be removed in the future without any notice.",
"Expired": "Expired",
"Expiration": "Expiration",
"Expiration_(Days)": "Expiration (Days)",
"Export_as_file": "Export as file",
@ -2309,7 +2310,9 @@
"Invitation_Subject": "Invitation Subject",
"Invitation_Subject_Default": "You have been invited to [Site_Name]",
"Invite": "Invite",
"Invites": "Invites",
"Invite_Link": "Invite Link",
"Invite_removed": "Invite removed successfully",
"Invite_user_to_join_channel": "Invite one user to join this channel",
"Invite_user_to_join_channel_all_from": "Invite all users from [#channel] to join this channel",
"Invite_user_to_join_channel_all_to": "Invite all users from this channel to join [#channel]",

Loading…
Cancel
Save