Refactor some React Pages and Components (#19202)

pull/19204/head^2
Tasso Evangelista 5 years ago committed by GitHub
parent 9716730901
commit fe0c27ab38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      client/account/AccountProfileForm.js
  2. 30
      client/account/AccountProfilePage.js
  3. 8
      client/account/AccountSidebar.js
  4. 48
      client/account/ActionConfirmModal.tsx
  5. 32
      client/account/preferences/MyDataModal.tsx
  6. 23
      client/account/preferences/PreferencesMyDataSection.js
  7. 36
      client/account/security/BackupCodesModal.tsx
  8. 73
      client/account/security/TwoFactorTOTP.js
  9. 55
      client/account/security/VerifyCodeModal.tsx
  10. 45
      client/account/tokens/AccountTokensRow.tsx
  11. 51
      client/account/tokens/AccountTokensTable.js
  12. 23
      client/account/tokens/InfoModal.tsx
  13. 40
      client/admin/apps/APIsDisplay.tsx
  14. 166
      client/admin/apps/AppDetailsPage.js
  15. 106
      client/admin/apps/AppDetailsPageContent.tsx
  16. 41
      client/admin/apps/AppLogsPage.js
  17. 4
      client/admin/apps/AppMenu.js
  18. 20
      client/admin/apps/AppProvider.tsx
  19. 94
      client/admin/apps/AppRow.tsx
  20. 10
      client/admin/apps/AppStatus.js
  21. 106
      client/admin/apps/AppsTable.js
  22. 4
      client/admin/apps/CloudLoginModal.js
  23. 4
      client/admin/apps/IframeModal.js
  24. 14
      client/admin/apps/LoadingDetails.tsx
  25. 31
      client/admin/apps/LogEntry.tsx
  26. 33
      client/admin/apps/LogItem.tsx
  27. 13
      client/admin/apps/LogsLoading.tsx
  28. 103
      client/admin/apps/MarketplaceRow.tsx
  29. 114
      client/admin/apps/MarketplaceTable.js
  30. 4
      client/admin/apps/PriceDisplay.js
  31. 50
      client/admin/apps/SettingsDisplay.tsx
  32. 27
      client/admin/apps/types.ts
  33. 87
      client/admin/cloud/CopyStep.tsx
  34. 150
      client/admin/cloud/ManualWorkspaceRegistrationModal.js
  35. 84
      client/admin/cloud/PasteStep.tsx
  36. 4
      client/admin/customEmoji/AddCustomEmoji.js
  37. 38
      client/admin/customEmoji/CustomEmoji.js
  38. 0
      client/admin/customEmoji/CustomEmoji.stories.js
  39. 11
      client/admin/customEmoji/CustomEmojiRoute.js
  40. 111
      client/admin/customEmoji/EditCustomEmoji.tsx
  41. 60
      client/admin/customEmoji/EditCustomEmojiWithData.tsx
  42. 6
      client/admin/customEmoji/types.ts
  43. 4
      client/admin/customSounds/AddCustomSound.js
  44. 40
      client/admin/customSounds/AdminSounds.js
  45. 6
      client/admin/customSounds/AdminSoundsRoute.js
  46. 58
      client/admin/customSounds/EditCustomSound.js
  47. 4
      client/admin/customUserStatus/AddCustomUserStatus.js
  48. 38
      client/admin/customUserStatus/CustomUserStatus.js
  49. 10
      client/admin/customUserStatus/CustomUserStatusRoute.js
  50. 89
      client/admin/customUserStatus/EditCustomUserStatus.js
  51. 49
      client/admin/customUserStatus/EditCustomUserStatusWithData.tsx
  52. 2
      client/admin/federationDashboard/FederationDashboardRoute.tsx
  53. 4
      client/admin/federationDashboard/OverviewSection.js
  54. 4
      client/admin/federationDashboard/ServersSection.js
  55. 95
      client/admin/import/PrepareChannels.tsx
  56. 140
      client/admin/import/PrepareImportPage.js
  57. 94
      client/admin/import/PrepareUsers.tsx
  58. 6
      client/admin/info/BuildEnvironmentSection.js
  59. 2
      client/admin/info/BuildEnvironmentSection.stories.js
  60. 6
      client/admin/info/CommitSection.js
  61. 2
      client/admin/info/CommitSection.stories.js
  62. 8
      client/admin/info/DescriptionList.js
  63. 2
      client/admin/info/DescriptionList.stories.js
  64. 16
      client/admin/info/InformationPage.js
  65. 2
      client/admin/info/InformationPage.stories.js
  66. 5
      client/admin/info/InformationRoute.js
  67. 6
      client/admin/info/InstancesSection.js
  68. 2
      client/admin/info/InstancesSection.stories.js
  69. 6
      client/admin/info/RocketChatSection.js
  70. 2
      client/admin/info/RocketChatSection.stories.js
  71. 6
      client/admin/info/RuntimeEnvironmentSection.js
  72. 2
      client/admin/info/RuntimeEnvironmentSection.stories.js
  73. 6
      client/admin/info/UsageSection.js
  74. 2
      client/admin/info/UsageSection.stories.js
  75. 23
      client/admin/integrations/IntegrationsTable.js
  76. 18
      client/admin/integrations/edit/EditIncomingWebhook.js
  77. 41
      client/admin/integrations/edit/EditIntegrationsPage.js
  78. 18
      client/admin/integrations/edit/EditOutgoingWebhook.js
  79. 2
      client/admin/invites/InvitesPage.js
  80. 25
      client/admin/mailer/MailerRoute.js
  81. 8
      client/admin/oauthApps/OAuthAppsTable.js
  82. 55
      client/admin/oauthApps/OAuthEditApp.js
  83. 2
      client/admin/permissions/PermissionsTable.js
  84. 10
      client/admin/permissions/UsersInRoleTable.js
  85. 24
      client/admin/rooms/RoomsTable.js
  86. 110
      client/admin/sidebar/AdminSidebar.js
  87. 20
      client/admin/sidebar/AdminSidebarPages.tsx
  88. 92
      client/admin/sidebar/AdminSidebarSettings.tsx
  89. 67
      client/admin/users/UserInfoActions.js
  90. 26
      client/admin/users/UsersTable.js
  91. 39
      client/channel/ExportMessages/index.js
  92. 33
      client/components/DeleteSuccessModal.tsx
  93. 26
      client/components/DeleteWarningModal.js
  94. 41
      client/components/DeleteWarningModal.tsx
  95. 37
      client/components/FilterByText.tsx
  96. 15
      client/components/GenericTable.stories.js
  97. 30
      client/components/GenericTable/HeaderCell.tsx
  98. 24
      client/components/GenericTable/LoadingRow.tsx
  99. 14
      client/components/GenericTable/SortIcon.tsx
  100. 61
      client/components/GenericTable/index.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,7 +13,7 @@ import UserStatusMenu from '../components/basic/userStatus/UserStatusMenu';
const STATUS_TEXT_MAX_LENGTH = 120;
export default function AccountProfileForm({ values, handlers, user, settings, onSaveStateChange, ...props }) {
function AccountProfileForm({ values, handlers, user, settings, onSaveStateChange, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
@ -245,3 +245,5 @@ export default function AccountProfileForm({ values, handlers, user, settings, o
<CustomFieldsForm customFieldsData={customFields} setCustomFieldsData={handleCustomFields}/>
</FieldGroup>;
}
export default AccountProfileForm;

@ -1,4 +1,4 @@
import { ButtonGroup, Button, Box, Icon, PasswordInput, TextInput, Modal } from '@rocket.chat/fuselage';
import { ButtonGroup, Button, Box, Icon } from '@rocket.chat/fuselage';
import { SHA256 } from 'meteor/sha';
import React, { useMemo, useState, useCallback } from 'react';
@ -14,33 +14,7 @@ import { useMethod } from '../contexts/ServerContext';
import { useSetModal } from '../contexts/ModalContext';
import { useUpdateAvatar } from '../hooks/useUpdateAvatar';
import { getUserEmailAddress } from '../helpers/getUserEmailAddress';
const ActionConfirmModal = ({ onSave, onCancel, title, text, isPassword, ...props }) => {
const t = useTranslation();
const [inputText, setInputText] = useState('');
const handleChange = useCallback((e) => setInputText(e.currentTarget.value), [setInputText]);
const handleSave = useCallback(() => { onSave(inputText); onCancel(); }, [inputText, onSave, onCancel]);
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8'>{text}</Box>
{isPassword && <PasswordInput w='full' value={inputText} onChange={handleChange}/>}
{!isPassword && <TextInput w='full' value={inputText} onChange={handleChange}/>}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={handleSave}>{t('Continue')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
import ActionConfirmModal from './ActionConfirmModal';
const getInitialValues = (user) => ({
realname: user.name ?? '',

@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import { useSubscription } from 'use-subscription';
import { menu, SideNav, Layout } from '../../app/ui-utils/client';
@ -8,7 +8,7 @@ import Sidebar from '../components/basic/Sidebar';
import SettingsProvider from '../providers/SettingsProvider';
import { itemsSubscription } from './sidebarItems';
export default React.memo(function AccountSidebar() {
const AccountSidebar = () => {
const t = useTranslation();
const items = useSubscription(itemsSubscription);
@ -40,4 +40,6 @@ export default React.memo(function AccountSidebar() {
</Sidebar.Content>
</Sidebar>
</SettingsProvider>;
});
};
export default memo(AccountSidebar);

@ -0,0 +1,48 @@
import { ButtonGroup, Button, Box, Icon, PasswordInput, TextInput, Modal } from '@rocket.chat/fuselage';
import React, { useState, useCallback, FC } from 'react';
import { useTranslation } from '../contexts/TranslationContext';
type ActionConfirmModalProps = {
title: string;
text: string;
isPassword: boolean;
onSave: (input: string) => void;
onCancel: () => void;
};
const ActionConfirmModal: FC<ActionConfirmModalProps> = ({
title,
text,
isPassword,
onSave,
onCancel,
...props
}) => {
const t = useTranslation();
const [inputText, setInputText] = useState('');
const handleChange = useCallback((e) => setInputText(e.currentTarget.value), [setInputText]);
const handleSave = useCallback(() => { onSave(inputText); onCancel(); }, [inputText, onSave, onCancel]);
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8'>{text}</Box>
{isPassword && <PasswordInput w='full' value={inputText} onChange={handleChange}/>}
{!isPassword && <TextInput w='full' value={inputText} onChange={handleChange}/>}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={handleSave}>{t('Continue')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default ActionConfirmModal;

@ -0,0 +1,32 @@
import React, { FC } from 'react';
import { ButtonGroup, Button, Icon, Box, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
type MyDataModalProps = {
onCancel: () => void;
title: string;
text: string;
};
const MyDataModal: FC<MyDataModalProps> = ({ onCancel, title, text, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='circle-check' size={20}/>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8'>{text}</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onCancel}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default MyDataModal;

@ -1,30 +1,11 @@
import React, { useCallback } from 'react';
import { Accordion, Field, FieldGroup, ButtonGroup, Button, Icon, Box, Modal } from '@rocket.chat/fuselage';
import { Accordion, Field, FieldGroup, ButtonGroup, Button, Icon, Box } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useMethod } from '../../contexts/ServerContext';
import { useSetModal } from '../../contexts/ModalContext';
const MyDataModal = ({ onCancel, title, text, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='circle-check' size={20}/>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8'>{text}</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onCancel}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
import MyDataModal from './MyDataModal';
const PreferencesMyDataSection = ({ onChange, ...props }) => {
const t = useTranslation();

@ -0,0 +1,36 @@
import React, { FC, useMemo } from 'react';
import { Box, Button, Icon, ButtonGroup, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import TextCopy from '../../components/basic/TextCopy';
type BackupCodesModalProps = {
codes: string[];
onClose: () => void;
};
const BackupCodesModal: FC<BackupCodesModalProps> = ({ codes, onClose, ...props }) => {
const t = useTranslation();
const codesText = useMemo(() => codes.join(' '), [codes]);
return <Modal {...props}>
<Modal.Header>
<Icon name='info' size={20}/>
<Modal.Title>{t('Backup_codes')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_1')}</Box>
<TextCopy text={codesText} wordBreak='break-word' mb='x8' />
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_2')}</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default BackupCodesModal;

@ -1,5 +1,5 @@
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { Box, Button, TextInput, Icon, ButtonGroup, Margins, Modal } from '@rocket.chat/fuselage';
import React, { useState, useCallback, useEffect } from 'react';
import { Box, Button, TextInput, Margins } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import qrcode from 'yaqrcode';
@ -10,73 +10,8 @@ import { useTranslation } from '../../contexts/TranslationContext';
import { useForm } from '../../hooks/useForm';
import { useMethod } from '../../contexts/ServerContext';
import TextCopy from '../../components/basic/TextCopy';
const BackupCodesModal = ({ codes, onClose, ...props }) => {
const t = useTranslation();
const codesText = useMemo(() => codes.join(' '), [codes]);
return <Modal {...props}>
<Modal.Header>
<Icon name='info' size={20}/>
<Modal.Title>{t('Backup_codes')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_1')}</Box>
<TextCopy text={codesText} wordBreak='break-word' mb='x8' />
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_2')}</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const VerifyCodeModal = ({ onVerify, onCancel, ...props }) => {
const t = useTranslation();
const ref = useRef();
useEffect(() => {
if (typeof ref?.current?.focus === 'function') {
ref.current.focus();
}
}, [ref]);
const { values, handlers } = useForm({ code: '' });
const { code } = values;
const { handleCode } = handlers;
const handleVerify = useCallback((e) => {
if (e.type === 'click' || (e.type === 'keydown' && e.keyCode === 13)) {
onVerify(code);
}
}, [code, onVerify]);
return <Modal {...props}>
<Modal.Header>
<Icon name='info' size={20}/>
<Modal.Title>{t('Two-factor_authentication')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mbe='x8'>{t('Open_your_authentication_app_and_enter_the_code')}</Box>
<Box display='flex' alignItems='stretch'>
<TextInput ref={ref} placeholder={t('Enter_authentication_code')} value={code} onChange={handleCode} onKeyDown={handleVerify}/>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button onClick={onCancel}>{t('Cancel')}</Button>
<Button primary onClick={handleVerify}>{t('Verify')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
import BackupCodesModal from './BackupCodesModal';
import VerifyCodeModal from './VerifyCodeModal';
const TwoFactorTOTP = (props) => {
const t = useTranslation();

@ -0,0 +1,55 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import { Box, Button, TextInput, Icon, ButtonGroup, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useForm } from '../../hooks/useForm';
type VerifyCodeModalProps = {
onVerify: (code: string) => void;
onCancel: () => void;
};
const VerifyCodeModal: FC<VerifyCodeModalProps> = ({ onVerify, onCancel, ...props }) => {
const t = useTranslation();
const ref = useRef<HTMLInputElement>();
useEffect(() => {
if (typeof ref?.current?.focus === 'function') {
ref.current.focus();
}
}, [ref]);
const { values, handlers } = useForm({ code: '' });
const { code } = values as { code: string };
const { handleCode } = handlers;
const handleVerify = useCallback((e) => {
if (e.type === 'click' || (e.type === 'keydown' && e.keyCode === 13)) {
onVerify(code);
}
}, [code, onVerify]);
return <Modal {...props}>
<Modal.Header>
<Icon name='info' size={20}/>
<Modal.Title>{t('Two-factor_authentication')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
<Box mbe='x8'>{t('Open_your_authentication_app_and_enter_the_code')}</Box>
<Box display='flex' alignItems='stretch'>
<TextInput ref={ref} placeholder={t('Enter_authentication_code')} value={code} onChange={handleCode} onKeyDown={handleVerify}/>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button onClick={onCancel}>{t('Cancel')}</Button>
<Button primary onClick={handleVerify}>{t('Verify')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default VerifyCodeModal;

@ -0,0 +1,45 @@
import { Button, ButtonGroup, Icon, Table } from '@rocket.chat/fuselage';
import React, { useCallback, FC } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
type AccountTokensRowProps = {
bypassTwoFactor: unknown;
createdAt: unknown;
isMedium: boolean;
lastTokenPart: string;
name: string;
onRegenerate: (name: string) => void;
onRemove: (name: string) => void;
};
const AccountTokensRow: FC<AccountTokensRowProps> = ({
bypassTwoFactor,
createdAt,
isMedium,
lastTokenPart,
name,
onRegenerate,
onRemove,
}) => {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]);
const handleRemove = useCallback(() => onRemove(name), [name, onRemove]);
return <Table.Row key={name} tabIndex={0} role='link' action qa-token-name={name}>
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell>
{isMedium && <Table.Cell withTruncatedText>{formatDateAndTime(createdAt)}</Table.Cell>}
<Table.Cell withTruncatedText>...{lastTokenPart}</Table.Cell>
<Table.Cell withTruncatedText>{bypassTwoFactor ? t('Ignore') : t('Require')}</Table.Cell>
<Table.Cell withTruncatedText>
<ButtonGroup>
<Button onClick={handleRegenerate} small><Icon name='refresh' size='x16'/></Button>
<Button onClick={handleRemove} small><Icon name='trash' size='x16'/></Button>
</ButtonGroup>
</Table.Cell>
</Table.Row>;
};
export default AccountTokensRow;

@ -1,37 +1,18 @@
import { Table, Button, ButtonGroup, Icon, Box } from '@rocket.chat/fuselage';
import { Box } from '@rocket.chat/fuselage';
import React, { useMemo, useCallback, useState } from 'react';
import { GenericTable, Th } from '../../components/GenericTable';
import { useSetModal } from '../../contexts/ModalContext';
import GenericTable from '../../components/GenericTable';
import { useMethod } from '../../contexts/ServerContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
import InfoModal from './InfoModal';
import { useUserId } from '../../contexts/UserContext';
import InfoModal from './InfoModal';
import AccountTokensRow from './AccountTokensRow';
const TokenRow = ({ lastTokenPart, name, createdAt, bypassTwoFactor, formatDateAndTime, onRegenerate, onRemove, t, isMedium }) => {
const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]);
const handleRemove = useCallback(() => onRemove(name), [name, onRemove]);
return <Table.Row key={name} tabIndex={0} role='link' action qa-token-name={name}>
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell>
{isMedium && <Table.Cell withTruncatedText>{formatDateAndTime(createdAt)}</Table.Cell>}
<Table.Cell withTruncatedText>...{lastTokenPart}</Table.Cell>
<Table.Cell withTruncatedText>{bypassTwoFactor ? t('Ignore') : t('Require')}</Table.Cell>
<Table.Cell withTruncatedText>
<ButtonGroup>
<Button onClick={handleRegenerate} small><Icon name='refresh' size='x16'/></Button>
<Button onClick={handleRemove} small><Icon name='trash' size='x16'/></Button>
</ButtonGroup>
</Table.Cell>
</Table.Row>;
};
export function AccountTokensTable({ data, reload }) {
const AccountTokensTable = ({ data, reload }) => {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const dispatchToastMessage = useToastMessageDispatch();
const setModal = useSetModal();
@ -58,11 +39,11 @@ export function AccountTokensTable({ data, reload }) {
const closeModal = useCallback(() => setModal(null), [setModal]);
const header = useMemo(() => [
<Th key={'name'}>{t('API_Personal_Access_Token_Name')}</Th>,
isMedium && <Th key={'createdAt'}>{t('Created_at')}</Th>,
<Th key={'lastTokenPart'}>{t('Last_token_part')}</Th>,
<Th key={'2fa'}>{t('Two Factor Authentication')}</Th>,
<Th key={'actions'} />,
<GenericTable.HeaderCell key={'name'}>{t('API_Personal_Access_Token_Name')}</GenericTable.HeaderCell>,
isMedium && <GenericTable.HeaderCell key={'createdAt'}>{t('Created_at')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'lastTokenPart'}>{t('Last_token_part')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'2fa'}>{t('Two Factor Authentication')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'actions'} />,
].filter(Boolean), [isMedium, t]);
const onRegenerate = useCallback((name) => {
@ -117,15 +98,13 @@ export function AccountTokensTable({ data, reload }) {
}, [closeModal, dispatchToastMessage, reload, removeToken, setModal, t]);
return <GenericTable ref={ref} header={header} results={tokens} total={tokensTotal} setParams={setParams} params={params}>
{useCallback((props) => <TokenRow
{useCallback((props) => <AccountTokensRow
onRegenerate={onRegenerate}
onRemove={onRemove}
t={t}
formatDateAndTime={formatDateAndTime}
isMedium={isMedium}
{...props}
/>, [formatDateAndTime, isMedium, onRegenerate, onRemove, t])}
/>, [isMedium, onRegenerate, onRemove])}
</GenericTable>;
}
};
export default AccountTokensTable;

@ -1,7 +1,26 @@
import React from 'react';
import React, { FC, ReactNode } from 'react';
import { Button, ButtonGroup, Modal } from '@rocket.chat/fuselage';
const InfoModal = ({ title, content, icon, onConfirm, onClose, confirmText, cancelText, ...props }) =>
type InfoModalProps = {
title: string;
content: ReactNode;
icon: ReactNode;
confirmText: string;
cancelText: string;
onConfirm: () => void;
onClose: () => void;
};
const InfoModal: FC<InfoModalProps> = ({
title,
content,
icon,
confirmText,
cancelText,
onConfirm,
onClose,
...props
}) =>
<Modal {...props}>
<Modal.Header>
{icon}

@ -0,0 +1,40 @@
import { Box, Divider } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useAbsoluteUrl } from '../../contexts/ServerContext';
import { apiCurlGetter } from './helpers';
type APIsDisplayProps = {
apis: {
path: string;
computedPath: string;
methods: unknown[];
examples: Record<string, unknown>;
}[];
};
const APIsDisplay: FC<APIsDisplayProps> = ({ apis }) => {
const t = useTranslation();
const absoluteUrl = useAbsoluteUrl();
const getApiCurl = apiCurlGetter(absoluteUrl);
return <>
<Divider />
<Box display='flex' flexDirection='column'>
<Box fontScale='s2' mb='x12'>{t('APIs')}</Box>
{apis.map((api) => <Box key={api.path} mb='x8'>
<Box fontScale='p2'>{api.methods.join(' | ').toUpperCase()} {api.path}</Box>
{api.methods.map((method) => <Box>
<Box withRichContent><pre><code>
{getApiCurl(method, api).map((curlAddress) => <>{curlAddress}<br /></>)}
</code></pre></Box>
</Box>)}
</Box>)}
</Box>
</>;
};
export default APIsDisplay;

@ -1,166 +1,18 @@
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { Button, ButtonGroup, Icon, Box, Divider, Chip, Margins, Skeleton, Throbber } from '@rocket.chat/fuselage';
import React, { useState, useCallback, useRef } from 'react';
import { Button, ButtonGroup, Icon, Box, Throbber } from '@rocket.chat/fuselage';
import Page from '../../components/basic/Page';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import ExternalLink from '../../components/basic/ExternalLink';
import PriceDisplay from './PriceDisplay';
import AppStatus from './AppStatus';
import AppMenu from './AppMenu';
import { useRoute, useCurrentRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useAppInfo } from './hooks/useAppInfo';
import { useAbsoluteUrl } from '../../contexts/ServerContext';
import { Apps } from '../../../app/apps/client/orchestrator';
import { useForm } from '../../hooks/useForm';
import { handleAPIError, apiCurlGetter } from './helpers';
import { AppSettingsAssembler } from './AppSettings';
import { handleAPIError } from './helpers';
import AppDetailsPageContent from './AppDetailsPageContent';
import SettingsDisplay from './SettingsDisplay';
import APIsDisplay from './APIsDisplay';
import LoadingDetails from './LoadingDetails';
function AppDetailsPageContent({ data }) {
const t = useTranslation();
const {
iconFileData = '',
name,
author: { name: authorName, homepage, support } = {},
description,
categories = [],
version,
price,
purchaseType,
pricingPlans,
iconFileContent,
installed,
bundledIn,
} = data;
return <>
<Box display='flex' flexDirection='row' mbe='x20' w='full'>
<AppAvatar size='x120' mie='x20' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}>
<Box fontScale='h1'>{name}</Box>
<Box display='flex' flexDirection='row' color='hint' alignItems='center'>
<Box fontScale='p2' mie='x4'>{t('By_author', { author: authorName })}</Box>
|
<Box mis= 'x4'>{t('Version_version', { version })}</Box>
</Box>
<Box display='flex' flexDirection='row' alignItems='center' justifyContent='space-between'>
<Box flexGrow={1} display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8'>
<AppStatus app={data} marginInline='x8'/>
{!installed && <PriceDisplay
purchaseType={purchaseType}
pricingPlans={pricingPlans}
price={price}
showType={false}
marginInline='x8'
/>}
</Box>
{installed && <AppMenu app={data} />}
</Box>
</Box>
</Box>
<Divider />
<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='s2'>{t('Categories')}</Box>
<Box display='flex' flexDirection='row'>
{categories && categories.map((current) => <Chip key={current} textTransform='uppercase' mie='x8'><Box color='hint'>{current}</Box></Chip>)}
</Box>
<Box fontScale='s2'>{t('Contact')}</Box>
<Box display='flex' flexDirection='row' flexGrow={1} justifyContent='space-around' flexWrap='wrap'>
<Box display='flex' flexDirection='column' mie='x12' flexGrow={1}>
<Box fontScale='s1' color='hint'>{t('Author_Site')}</Box>
<ExternalLink to={homepage} />
</Box>
<Box display='flex' flexDirection='column' flexGrow={1}>
<Box fontScale='s1' color='hint'>{t('Support')}</Box>
<ExternalLink to={support} />
</Box>
</Box>
<Box fontScale='s2'>{t('Details')}</Box>
<Box display='flex' flexDirection='row'>{description}</Box>
</Margins>
</Box>
{bundledIn && <>
<Divider />
<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='s2'>{t('Bundles')}</Box>
{bundledIn.map((bundle) => <Box key={bundle.bundleId} display='flex' flexDirection='row' alignItems='center'>
<Box width='x80' height='x80' display='flex' flexDirection='row' justifyContent='space-around' flexWrap='wrap' flexShrink={0}>
{bundle.apps.map((app) => <AppAvatar size='x36' key={app.latest.name} iconFileContent={app.latest.iconFileContent} iconFileData={app.latest.iconFileData}/>)}
</Box>
<Box display='flex' flexDirection='column' mis='x12'>
<Box fontScale='p2'>{bundle.bundleName}</Box>
{bundle.apps.map((app) => <Box key={app.latest.name}>{app.latest.name},</Box>)}
</Box>
</Box>)}
</Margins>
</Box>
</>}
</>;
}
const SettingsDisplay = ({ settings, setHasUnsavedChanges, settingsRef }) => {
const t = useTranslation();
const reducedSettings = useMemo(() => Object.values(settings).reduce((ret, { id, value, packageValue }) => {
ret = { ...ret, [id]: value ?? packageValue };
return ret;
}, {}), [JSON.stringify(settings)]);
const { values, handlers, hasUnsavedChanges } = useForm(reducedSettings);
useEffect(() => {
setHasUnsavedChanges(hasUnsavedChanges);
settingsRef.current = values;
}, [hasUnsavedChanges, JSON.stringify(values), setHasUnsavedChanges]);
return <>
<Divider />
<Box display='flex' flexDirection='column'>
<Box fontScale='s2' mb='x12'>{t('Settings')}</Box>
<AppSettingsAssembler settings={settings} values={values} handlers={handlers}/>
</Box>
</>;
};
const APIsDisplay = ({ apis }) => {
const t = useTranslation();
const absoluteUrl = useAbsoluteUrl();
const getApiCurl = apiCurlGetter(absoluteUrl);
return <>
<Divider />
<Box display='flex' flexDirection='column'>
<Box fontScale='s2' mb='x12'>{t('APIs')}</Box>
{apis.map((api) => <Box mb='x8'>
<Box fontScale='p2'>{api.methods.join(' | ').toUpperCase()} {api.path}</Box>
{api.methods.map((method) => <Box>
<Box withRichContent><pre><code>
{getApiCurl(method, api).map((curlAddress) => <>{curlAddress}<br /></>)}
</code></pre></Box>
</Box>)}
</Box>)}
</Box>
</>;
};
const LoadingDetails = () => <Box display='flex' flexDirection='row' mbe='x20' w='full'>
<Skeleton variant='rect' w='x120' h='x120' mie='x20'/>
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}>
<Skeleton variant='rect' w='full' h='x32'/>
<Skeleton variant='rect' w='full' h='x32'/>
<Skeleton variant='rect' w='full' h='x32'/>
</Box>
</Box>;
export default function AppDetailsPage({ id }) {
function AppDetailsPage({ id }) {
const t = useTranslation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
@ -216,3 +68,5 @@ export default function AppDetailsPage({ id }) {
</Page.ScrollableContentWithShadow>
</Page>;
}
export default AppDetailsPage;

@ -0,0 +1,106 @@
import React, { FC } from 'react';
import { Box, Chip, Divider, Margins } from '@rocket.chat/fuselage';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import ExternalLink from '../../components/basic/ExternalLink';
import PriceDisplay from './PriceDisplay';
import AppStatus from './AppStatus';
import AppMenu from './AppMenu';
import { useTranslation } from '../../contexts/TranslationContext';
import { App } from './types';
type AppDetailsPageContentProps = {
data: App;
};
const AppDetailsPageContent: FC<AppDetailsPageContentProps> = ({ data }) => {
const t = useTranslation();
const {
iconFileData = '',
name,
author: { name: authorName, homepage, support },
description,
categories = [],
version,
price,
purchaseType,
pricingPlans,
iconFileContent,
installed,
bundledIn,
} = data;
return <>
<Box display='flex' flexDirection='row' mbe='x20' w='full'>
<AppAvatar size='x120' mie='x20' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}>
<Box fontScale='h1'>{name}</Box>
<Box display='flex' flexDirection='row' color='hint' alignItems='center'>
<Box fontScale='p2' mie='x4'>{t('By_author', { author: authorName })}</Box>
|
<Box mis='x4'>{t('Version_version', { version })}</Box>
</Box>
<Box display='flex' flexDirection='row' alignItems='center' justifyContent='space-between'>
<Box flexGrow={1} display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8'>
<AppStatus app={data} marginInline='x8'/>
{!installed && <PriceDisplay
purchaseType={purchaseType}
pricingPlans={pricingPlans}
price={price}
showType={false}
marginInline='x8'
/>}
</Box>
{installed && <AppMenu app={data} />}
</Box>
</Box>
</Box>
<Divider />
<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='s2'>{t('Categories')}</Box>
<Box display='flex' flexDirection='row'>
{categories && categories.map((current) =>
<Chip key={current} textTransform='uppercase' mie='x8'>
<Box color='hint'>{current}</Box>
</Chip>)}
</Box>
<Box fontScale='s2'>{t('Contact')}</Box>
<Box display='flex' flexDirection='row' flexGrow={1} justifyContent='space-around' flexWrap='wrap'>
<Box display='flex' flexDirection='column' mie='x12' flexGrow={1}>
<Box fontScale='s1' color='hint'>{t('Author_Site')}</Box>
<ExternalLink to={homepage} />
</Box>
<Box display='flex' flexDirection='column' flexGrow={1}>
<Box fontScale='s1' color='hint'>{t('Support')}</Box>
<ExternalLink to={support} />
</Box>
</Box>
<Box fontScale='s2'>{t('Details')}</Box>
<Box display='flex' flexDirection='row'>{description}</Box>
</Margins>
</Box>
{bundledIn && <>
<Divider />
<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='s2'>{t('Bundles')}</Box>
{bundledIn.map((bundle) => <Box key={bundle.bundleId} display='flex' flexDirection='row' alignItems='center'>
<Box width='x80' height='x80' display='flex' flexDirection='row' justifyContent='space-around' flexWrap='wrap' flexShrink={0}>
{bundle.apps.map((app) => <AppAvatar size='x36' key={app.latest.name} iconFileContent={app.latest.iconFileContent} iconFileData={app.latest.iconFileData}/>)}
</Box>
<Box display='flex' flexDirection='column' mis='x12'>
<Box fontScale='p2'>{bundle.bundleName}</Box>
{bundle.apps.map((app) => <Box key={app.latest.name}>{app.latest.name},</Box>)}
</Box>
</Box>)}
</Margins>
</Box>
</>}
</>;
};
export default AppDetailsPageContent;

@ -1,4 +1,4 @@
import { Box, Button, ButtonGroup, Icon, Accordion, Skeleton, Margins, Pagination } from '@rocket.chat/fuselage';
import { Box, Button, ButtonGroup, Icon, Accordion, Pagination } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useEffect } from 'react';
@ -6,43 +6,9 @@ import Page from '../../components/basic/Page';
import { useCurrentRoute, useRoute } from '../../contexts/RouterContext';
import { useEndpoint } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useHighlightedCode } from '../../hooks/useHighlightedCode';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
const LogEntry = ({ severity, timestamp, caller, args }) => {
const t = useTranslation();
return <Box>
<Box>{severity}: {timestamp} {t('Caller')}: {caller}</Box>
<Box withRichContent width='full'>
<pre>
<code
dangerouslySetInnerHTML={{
__html: useHighlightedCode('json', JSON.stringify(args, null, 2)),
}}
/>
</pre>
</Box>
</Box>;
};
const LogItem = ({ entries, instanceId, title, t, ...props }) => <Accordion.Item title={title} {...props}>
{instanceId && <Box>{t('Instance')}: {instanceId}</Box>}
{entries.map(({ severity, timestamp, caller, args }, i) => <LogEntry
key={i}
severity={severity}
timestamp={timestamp}
caller={caller}
args={args}
/>)}
</Accordion.Item>;
const LogsLoading = () => <Box maxWidth='x600' w='full' alignSelf='center'>
<Margins block='x2'>
<Skeleton variant='rect' width='100%' height='x80' />
<Skeleton variant='rect' width='100%' height='x80' />
<Skeleton variant='rect' width='100%' height='x80' />
</Margins>
</Box>;
import LogItem from './LogItem';
import LogsLoading from './LogsLoading';
const useAppWithLogs = ({ id, current, itemsPerPage }) => {
const [data, setData] = useSafely(useState({}));
@ -123,7 +89,6 @@ function AppLogsPage({ id, ...props }) {
title={`${ formatDateAndTime(log._createdAt) }: "${ log.method }" (${ log.totalTime }ms)`}
instanceId={log.instanceId}
entries={log.entries}
t={t}
/>)}
</Accordion>
</>}

@ -6,8 +6,8 @@ import { useRoute } from '../../contexts/RouterContext';
import { useMethod, useEndpoint } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { appEnabledStatuses, warnStatusChange, handleAPIError } from './helpers';
import { CloudLoginModal } from './CloudLoginModal';
import { IframeModal } from './IframeModal';
import CloudLoginModal from './CloudLoginModal';
import IframeModal from './IframeModal';
import WarningModal from './WarningModal';
function AppMenu({ app, ...props }) {

@ -9,17 +9,7 @@ import React, {
import { Apps } from '../../../app/apps/client/orchestrator';
import { AppEvents } from '../../../app/apps/client/communication';
import { handleAPIError } from './helpers';
type App = {
id: string;
name: string;
status: unknown;
installed: boolean;
marketplace: unknown;
version: unknown;
marketplaceVersion: unknown;
bundledIn: unknown;
};
import { App } from './types';
export type AppDataContextValue = {
data: App[];
@ -143,7 +133,7 @@ const AppProvider: FunctionComponent = ({ children }) => {
const marketplaceApps = await Apps.getAppsFromMarketplace() as App[];
const appsData = marketplaceApps.length ? marketplaceApps.map((app) => {
const appsData = marketplaceApps.length ? marketplaceApps.map<App>((app) => {
const appIndex = installedApps.findIndex(({ id }) => id === app.id);
if (!installedApps[appIndex]) {
return {
@ -158,8 +148,10 @@ const AppProvider: FunctionComponent = ({ children }) => {
return {
...app,
installed: true,
status: installedApp?.status,
version: installedApp?.version,
...installedApp && {
status: installedApp.status,
version: installedApp.version,
},
bundledIn: app.bundledIn,
marketplaceVersion: app.version,
};

@ -0,0 +1,94 @@
import { Box, Table, Tag } from '@rocket.chat/fuselage';
import React, { FC, useState, memo, KeyboardEvent, MouseEvent } from 'react';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import { App } from './types';
type AppRowProps = App & {
medium: boolean;
};
const AppRow: FC<AppRowProps> = ({
medium,
...props
}) => {
const {
author: { name: authorName },
name,
id,
description,
categories,
iconFileData,
marketplaceVersion,
iconFileContent,
installed,
} = props;
const t = useTranslation();
const [isFocused, setFocused] = useState(false);
const [isHovered, setHovered] = useState(false);
const isStatusVisible = isFocused || isHovered;
const appsRoute = useRoute('admin-apps');
const handleClick = (): void => {
appsRoute.push({
context: 'details',
version: marketplaceVersion,
id,
});
};
const handleKeyDown = (e: KeyboardEvent<HTMLOrSVGElement>): void => {
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) {
return;
}
handleClick();
};
const preventClickPropagation = (e: MouseEvent<HTMLOrSVGElement>): void => {
e.stopPropagation();
};
return <Table.Row
key={id}
role='link'
action
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={(): void => setFocused(true)}
onBlur={(): void => setFocused(false)}
onMouseEnter={(): void => setHovered(true)}
onMouseLeave={(): void => setHovered(false)}
>
<Table.Cell withTruncatedText display='flex' flexDirection='row'>
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' alignSelf='flex-start'>
<Box color='default' fontScale='p2'>{name}</Box>
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box>
</Box>
</Table.Cell>
{medium && <Table.Cell>
<Box display='flex' flexDirection='column'>
<Box color='default' withTruncatedText>{description}</Box>
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText>
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)}
</Box>}
</Box>
</Table.Cell>}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/>
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>}
</Box>
</Table.Cell>
</Table.Row>;
};
export default memo(AppRow);

@ -5,8 +5,8 @@ import React, { useCallback, useState, memo } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange } from './helpers';
import { Apps } from '../../../app/apps/client/orchestrator';
import { IframeModal } from './IframeModal';
import { CloudLoginModal } from './CloudLoginModal';
import IframeModal from './IframeModal';
import CloudLoginModal from './CloudLoginModal';
import { useSetModal } from '../../contexts/ModalContext';
import { useMethod } from '../../contexts/ServerContext';
@ -32,7 +32,7 @@ const actions = {
},
};
const AppStatus = memo(({ app, showStatus = true, ...props }) => {
const AppStatus = ({ app, showStatus = true, ...props }) => {
const t = useTranslation();
const [loading, setLoading] = useSafely(useState());
const setModal = useSetModal();
@ -97,6 +97,6 @@ const AppStatus = memo(({ app, showStatus = true, ...props }) => {
{t(status.label)}
</Box>}
</Box>;
});
};
export default AppStatus;
export default memo(AppStatus);

@ -1,111 +1,13 @@
import { Box, Icon, Table, Tag, TextInput } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useEffect, memo, useContext, useMemo } from 'react';
import React, { useState, useContext, useMemo } from 'react';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import GenericTable from '../../components/GenericTable';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useFilteredApps } from './hooks/useFilteredApps';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import { AppDataContext } from './AppProvider';
const FilterByText = memo(({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput placeholder={t('Search_Apps')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
});
const AppRow = memo(function AppRow({
medium,
...props
}) {
const {
author: { name: authorName },
name,
id,
description,
categories,
iconFileData,
marketplaceVersion,
iconFileContent,
installed,
} = props;
const t = useTranslation();
const [isFocused, setFocused] = useState(false);
const [isHovered, setHovered] = useState(false);
const isStatusVisible = isFocused || isHovered;
const appsRoute = useRoute('admin-apps');
const handleClick = () => {
appsRoute.push({
context: 'details',
version: marketplaceVersion,
id,
});
};
const handleKeyDown = (e) => {
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) {
return;
}
handleClick();
};
const preventClickPropagation = (e) => {
e.stopPropagation();
};
return <Table.Row
key={id}
role='link'
action
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Table.Cell withTruncatedText display='flex' flexDirection='row'>
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' alignSelf='flex-start'>
<Box color='default' fontScale='p2'>{name}</Box>
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box>
</Box>
</Table.Cell>
{medium && <Table.Cell>
<Box display='flex' flexDirection='column'>
<Box color='default' withTruncatedText>{description}</Box>
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText>
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)}
</Box>}
</Box>
</Table.Cell>}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/>
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>}
</Box>
</Table.Cell>
</Table.Row>;
});
import AppRow from './AppRow';
import FilterByText from '../../components/FilterByText';
function AppsTable() {
const t = useTranslation();
@ -163,7 +65,7 @@ function AppsTable() {
total={filteredAppsCount}
params={params}
setParams={setParams}
FilterComponent={FilterByText}
renderFilter={({ onChange, ...props }) => <FilterByText placeholder={t('Search_Apps')} onChange={onChange} {...props} />}
>
{(props) => <AppRow key={props.id} medium={onMediumBreakpoint} {...props} />}
</GenericTable>;

@ -5,7 +5,7 @@ import { useSetModal } from '../../contexts/ModalContext';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
export const CloudLoginModal = (props) => {
const CloudLoginModal = (props) => {
const t = useTranslation();
const setModal = useSetModal();
const cloudRoute = useRoute('cloud');
@ -40,3 +40,5 @@ export const CloudLoginModal = (props) => {
</Modal.Footer>
</Modal>;
};
export default CloudLoginModal;

@ -12,7 +12,7 @@ const iframeMsgListener = (confirm, cancel) => (e) => {
data.result ? confirm(data) : cancel();
};
export const IframeModal = ({ url, confirm, cancel, ...props }) => {
const IframeModal = ({ url, confirm, cancel, ...props }) => {
useEffect(() => {
const listener = iframeMsgListener(confirm, cancel);
@ -29,3 +29,5 @@ export const IframeModal = ({ url, confirm, cancel, ...props }) => {
</Box>
</Modal>;
};
export default IframeModal;

@ -0,0 +1,14 @@
import React, { FC } from 'react';
import { Box, Skeleton } from '@rocket.chat/fuselage';
const LoadingDetails: FC = () =>
<Box display='flex' flexDirection='row' mbe='x20' w='full'>
<Skeleton variant='rect' w='x120' h='x120' mie='x20'/>
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}>
<Skeleton variant='rect' w='full' h='x32'/>
<Skeleton variant='rect' w='full' h='x32'/>
<Skeleton variant='rect' w='full' h='x32'/>
</Box>
</Box>;
export default LoadingDetails;

@ -0,0 +1,31 @@
import { Box } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useHighlightedCode } from '../../hooks/useHighlightedCode';
type LogEntryProps = {
severity: string;
timestamp: string;
caller: string;
args: unknown;
};
const LogEntry: FC<LogEntryProps> = ({ severity, timestamp, caller, args }) => {
const t = useTranslation();
return <Box>
<Box>{severity}: {timestamp} {t('Caller')}: {caller}</Box>
<Box withRichContent width='full'>
<pre>
<code
dangerouslySetInnerHTML={{
__html: useHighlightedCode('json', JSON.stringify(args, null, 2)),
}}
/>
</pre>
</Box>
</Box>;
};
export default LogEntry;

@ -0,0 +1,33 @@
import { Box, Accordion } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import LogEntry from './LogEntry';
type LogItemProps = {
entries: {
severity: string;
timestamp: string;
caller: string;
args: unknown;
}[];
instanceId: string;
title: string;
};
const LogItem: FC<LogItemProps> = ({ entries, instanceId, title, ...props }) => {
const t = useTranslation();
return <Accordion.Item title={title} {...props}>
{instanceId && <Box>{t('Instance')}: {instanceId}</Box>}
{entries.map(({ severity, timestamp, caller, args }, i) => <LogEntry
key={i}
severity={severity}
timestamp={timestamp}
caller={caller}
args={args}
/>)}
</Accordion.Item>;
};
export default LogItem;

@ -0,0 +1,13 @@
import { Box, Skeleton, Margins } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
const LogsLoading: FC = () =>
<Box maxWidth='x600' w='full' alignSelf='center'>
<Margins block='x2'>
<Skeleton variant='rect' width='100%' height='x80' />
<Skeleton variant='rect' width='100%' height='x80' />
<Skeleton variant='rect' width='100%' height='x80' />
</Margins>
</Box>;
export default LogsLoading;

@ -0,0 +1,103 @@
import { Box, Table, Tag } from '@rocket.chat/fuselage';
import React, { useState, memo, FC, KeyboardEvent, MouseEvent } from 'react';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import PriceDisplay from './PriceDisplay';
import { App } from './types';
type MarketplaceRowProps = {
medium?: boolean;
large?: boolean;
} & App;
const MarketplaceRow: FC<MarketplaceRowProps> = ({
medium,
large,
...props
}) => {
const {
author: { name: authorName },
name,
id,
description,
categories,
purchaseType,
pricingPlans,
price,
iconFileData,
marketplaceVersion,
iconFileContent,
installed,
} = props;
const t = useTranslation();
const [isFocused, setFocused] = useState(false);
const [isHovered, setHovered] = useState(false);
const isStatusVisible = isFocused || isHovered;
const marketplaceRoute = useRoute('admin-marketplace');
const handleClick = (): void => {
marketplaceRoute.push({
context: 'details',
version: marketplaceVersion,
id,
});
};
const handleKeyDown = (e: KeyboardEvent<HTMLOrSVGElement>): void => {
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) {
return;
}
handleClick();
};
const preventClickPropagation = (e: MouseEvent<HTMLOrSVGElement>): void => {
e.stopPropagation();
};
return <Table.Row
key={id}
role='link'
action
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={(): void => setFocused(true)}
onBlur={(): void => setFocused(false)}
onMouseEnter={(): void => setHovered(true)}
onMouseLeave={(): void => setHovered(false)}
>
<Table.Cell withTruncatedText display='flex' flexDirection='row'>
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' alignSelf='flex-start'>
<Box color='default' fontScale='p2'>{name}</Box>
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box>
</Box>
</Table.Cell>
{large && <Table.Cell>
<Box display='flex' flexDirection='column'>
<Box color='default' withTruncatedText>{description}</Box>
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText>
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)}
</Box>}
</Box>
</Table.Cell>}
{medium && <Table.Cell>
<PriceDisplay {...{ purchaseType, pricingPlans, price }} />
</Table.Cell>}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/>
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>}
</Box>
</Table.Cell>
</Table.Row>;
};
export default memo(MarketplaceRow);

@ -1,119 +1,13 @@
import { Box, Icon, Table, Tag, TextInput } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useEffect, useContext, useMemo, memo } from 'react';
import React, { useCallback, useState, useContext, useMemo } from 'react';
import AppAvatar from '../../components/basic/avatar/AppAvatar';
import GenericTable from '../../components/GenericTable';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useFilteredApps } from './hooks/useFilteredApps';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import PriceDisplay from './PriceDisplay';
import { AppDataContext } from './AppProvider';
const FilterByText = React.memo(({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput placeholder={t('Search_Apps')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
});
const MarketplaceRow = memo(function MarketplaceRow({
medium,
large,
...props
}) {
const {
author: { name: authorName },
name,
id,
description,
categories,
purchaseType,
pricingPlans,
price,
iconFileData,
marketplaceVersion,
iconFileContent,
installed,
} = props;
const t = useTranslation();
const [isFocused, setFocused] = useState(false);
const [isHovered, setHovered] = useState(false);
const isStatusVisible = isFocused || isHovered;
const marketplaceRoute = useRoute('admin-marketplace');
const handleClick = () => {
marketplaceRoute.push({
context: 'details',
version: marketplaceVersion,
id,
});
};
const handleKeyDown = (e) => {
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) {
return;
}
handleClick();
};
const preventClickPropagation = (e) => {
e.stopPropagation();
};
return <Table.Row
key={id}
role='link'
action
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Table.Cell withTruncatedText display='flex' flexDirection='row'>
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/>
<Box display='flex' flexDirection='column' alignSelf='flex-start'>
<Box color='default' fontScale='p2'>{name}</Box>
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box>
</Box>
</Table.Cell>
{large && <Table.Cell>
<Box display='flex' flexDirection='column'>
<Box color='default' withTruncatedText>{description}</Box>
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText>
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)}
</Box>}
</Box>
</Table.Cell>}
{medium && <Table.Cell>
<PriceDisplay {...{ purchaseType, pricingPlans, price }} />
</Table.Cell>}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/>
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>}
</Box>
</Table.Cell>
</Table.Row>;
});
import MarketplaceRow from './MarketplaceRow';
import FilterByText from '../../components/FilterByText';
function MarketplaceTable() {
const t = useTranslation();
@ -178,7 +72,7 @@ function MarketplaceTable() {
total={filteredAppsCount}
setParams={setParams}
params={params}
FilterComponent={FilterByText}
renderFilter={({ onChange, ...props }) => <FilterByText placeholder={t('Search_Apps')} onChange={onChange} {...props} />}
>
{(props) => <MarketplaceRow
key={props.id}

@ -20,7 +20,7 @@ const formatPriceAndPurchaseType = (purchaseType, pricingPlans, price) => {
return { type: 'Free', price: '-' };
};
export default function PriceDisplay({ purchaseType, pricingPlans, price, showType = true, ...props }) {
function PriceDisplay({ purchaseType, pricingPlans, price, showType = true, ...props }) {
const t = useTranslation();
const { type, price: formatedPrice } = useMemo(() => formatPriceAndPurchaseType(purchaseType, pricingPlans, price), [purchaseType, pricingPlans, price]);
@ -29,3 +29,5 @@ export default function PriceDisplay({ purchaseType, pricingPlans, price, showTy
<Box color='hint' withTruncatedText>{!showType && type === 'Free' ? t(type) : formatedPrice}</Box>
</Box>;
}
export default PriceDisplay;

@ -0,0 +1,50 @@
import React, { FC, useMemo, useEffect, MutableRefObject } from 'react';
import { Box, Divider } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useForm } from '../../hooks/useForm';
import { AppSettingsAssembler } from './AppSettings';
import { ISetting } from '../../../definition/ISetting';
type SettingsDisplayProps = {
settings: {
[id: string]: ISetting & { id: ISetting['_id'] };
};
setHasUnsavedChanges: (hasUnsavedChanges: boolean) => void;
settingsRef: MutableRefObject<Record<string, ISetting['value']>>;
};
const SettingsDisplay: FC<SettingsDisplayProps> = ({
settings,
setHasUnsavedChanges,
settingsRef,
}) => {
const t = useTranslation();
const stringifiedSettings = JSON.stringify(settings);
const reducedSettings = useMemo(() => {
const settings: SettingsDisplayProps['settings'] = JSON.parse(stringifiedSettings);
return Object.values(settings)
.reduce((ret, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), {});
}, [stringifiedSettings]);
const { values, handlers, hasUnsavedChanges } = useForm(reducedSettings);
const stringifiedValues = JSON.stringify(values);
useEffect(() => {
const values = JSON.parse(stringifiedValues);
setHasUnsavedChanges(hasUnsavedChanges);
settingsRef.current = values;
}, [hasUnsavedChanges, stringifiedValues, setHasUnsavedChanges, settingsRef]);
return <>
<Divider />
<Box display='flex' flexDirection='column'>
<Box fontScale='s2' mb='x12'>{t('Settings')}</Box>
<AppSettingsAssembler settings={settings} values={values} handlers={handlers}/>
</Box>
</>;
};
export default SettingsDisplay;

@ -0,0 +1,27 @@
export type App = {
id: string;
iconFileData: string;
name: string;
author: {
name: string;
homepage: string;
support: string;
};
description: string;
categories: string[];
version: string;
price: string;
purchaseType: unknown[];
pricingPlans: unknown[];
iconFileContent: unknown;
installed?: boolean;
bundledIn: {
bundleId: string;
bundleName: string;
apps: App[];
}[];
marketplaceVersion: string;
latest: App;
status: unknown;
marketplace: unknown;
};

@ -0,0 +1,87 @@
import { Box, Button, ButtonGroup, Icon, Scrollable, Modal } from '@rocket.chat/fuselage';
import Clipboard from 'clipboard';
import React, { useEffect, useState, useRef, FC } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import MarkdownText from '../../components/basic/MarkdownText';
import { cloudConsoleUrl } from './constants';
type CopyStepProps = {
onNextButtonClick: () => void;
};
const CopyStep: FC<CopyStepProps> = ({ onNextButtonClick }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [clientKey, setClientKey] = useState('');
const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData');
useEffect(() => {
const loadWorkspaceRegisterData = async (): Promise<void> => {
const clientKey = await getWorkspaceRegisterData();
setClientKey(clientKey);
};
loadWorkspaceRegisterData();
}, [getWorkspaceRegisterData]);
const copyRef = useRef<Element>();
useEffect(() => {
if (!copyRef.current) {
return;
}
const clipboard = new Clipboard(copyRef.current);
clipboard.on('success', () => {
dispatchToastMessage({ type: 'success', message: t('Copied') });
});
return (): void => {
clipboard.destroy();
};
}, [dispatchToastMessage, t]);
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all' }}
>
{clientKey}
</Box>
</Scrollable>
<Button ref={copyRef} primary data-clipboard-text={clientKey}>
<Icon name='copy' /> {t('Copy')}
</Button>
</Box>
<MarkdownText is='p' preserveHtml={true} withRichContent content={t('Cloud_click_here', { cloudConsoleUrl })} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button primary onClick={onNextButtonClick}>{t('Next')}</Button>
</ButtonGroup>
</Modal.Footer>
</>;
};
export default CopyStep;

@ -1,151 +1,9 @@
import { Box, Button, ButtonGroup, Icon, Scrollable, Throbber, Modal } from '@rocket.chat/fuselage';
import Clipboard from 'clipboard';
import React, { useEffect, useState, useRef } from 'react';
import { Modal } from '@rocket.chat/fuselage';
import React, { useState } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod, useEndpoint } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import MarkdownText from '../../components/basic/MarkdownText';
import { cloudConsoleUrl } from './constants';
function CopyStep({ onNextButtonClick }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [clientKey, setClientKey] = useState('');
const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData');
useEffect(() => {
const loadWorkspaceRegisterData = async () => {
const clientKey = await getWorkspaceRegisterData();
setClientKey(clientKey);
};
loadWorkspaceRegisterData();
}, [getWorkspaceRegisterData]);
const copyRef = useRef();
useEffect(function() {
const clipboard = new Clipboard(copyRef.current);
clipboard.on('success', () => {
dispatchToastMessage({ type: 'success', message: t('Copied') });
});
return () => {
clipboard.destroy();
};
}, [dispatchToastMessage, t]);
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all' }}
>
{clientKey}
</Box>
</Scrollable>
<Button ref={copyRef} primary data-clipboard-text={clientKey}>
<Icon name='copy' /> {t('Copy')}
</Button>
</Box>
<MarkdownText is='p' preserveHtml={true} withRichContent content={t('Cloud_click_here', { cloudConsoleUrl })} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button primary onClick={onNextButtonClick}>{t('Next')}</Button>
</ButtonGroup>
</Modal.Footer>
</>;
}
function PasteStep({ onBackButtonClick, onFinish }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [isLoading, setLoading] = useState(false);
const [cloudKey, setCloudKey] = useState('');
const handleCloudKeyChange = (e) => {
setCloudKey(e.currentTarget.value);
};
const registerManually = useEndpoint('POST', 'cloud.manualRegister');
const handleFinishButtonClick = async () => {
setLoading(true);
try {
await registerManually({}, { cloudBlob: cloudKey });
dispatchToastMessage({ type: 'success', message: t('Cloud_register_success') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: t('Cloud_register_error') });
} finally {
setLoading(false);
onFinish && onFinish();
}
};
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_finish_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
is='textarea'
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all', resize: 'none' }}
placeholder={t('Paste_here')}
disabled={isLoading}
value={cloudKey}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
onChange={handleCloudKeyChange}
/>
</Scrollable>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button>
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}>
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')}
</Button>
</ButtonGroup>
</Modal.Footer>
</>;
}
import CopyStep from './CopyStep';
import PasteStep from './PasteStep';
const Steps = {
COPY: 'copy',

@ -0,0 +1,84 @@
import { Box, Button, ButtonGroup, Scrollable, Throbber, Modal } from '@rocket.chat/fuselage';
import React, { ChangeEvent, FC, useState } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpoint } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
type PasteStepProps = {
onBackButtonClick: () => void;
onFinish: () => void;
};
const PasteStep: FC<PasteStepProps> = ({ onBackButtonClick, onFinish }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [isLoading, setLoading] = useState(false);
const [cloudKey, setCloudKey] = useState('');
const handleCloudKeyChange = (e: ChangeEvent<HTMLInputElement>): void => {
setCloudKey(e.currentTarget.value);
};
const registerManually = useEndpoint('POST', 'cloud.manualRegister');
const handleFinishButtonClick = async (): Promise<void> => {
setLoading(true);
try {
await registerManually({}, { cloudBlob: cloudKey });
dispatchToastMessage({ type: 'success', message: t('Cloud_register_success') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: t('Cloud_register_error') });
} finally {
setLoading(false);
onFinish && onFinish();
}
};
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_finish_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
is='textarea'
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all', resize: 'none' }}
placeholder={t('Paste_here')}
disabled={isLoading}
value={cloudKey}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
onChange={handleCloudKeyChange}
/>
</Scrollable>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button>
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}>
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')}
</Button>
</ButtonGroup>
</Modal.Footer>
</>;
};
export default PasteStep;

@ -6,7 +6,7 @@ import { useFileInput } from '../../hooks/useFileInput';
import { useEndpointUpload } from '../../hooks/useEndpointUpload';
import VerticalBar from '../../components/basic/VerticalBar';
export function AddCustomEmoji({ close, onChange, ...props }) {
function AddCustomEmoji({ close, onChange, ...props }) {
const t = useTranslation();
const [name, setName] = useState('');
@ -77,3 +77,5 @@ export function AddCustomEmoji({ close, onChange, ...props }) {
</Field>
</VerticalBar.ScrollableContent>;
}
export default AddCustomEmoji;

@ -1,23 +1,11 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import { Box, Table } from '@rocket.chat/fuselage';
import { GenericTable, Th } from '../../components/GenericTable';
import FilterByText from '../../components/FilterByText';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
const FilterByText = ({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput flexShrink={0} placeholder={t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
};
export function CustomEmoji({
function CustomEmoji({
data,
sort,
onClick,
@ -28,8 +16,8 @@ export function CustomEmoji({
const t = useTranslation();
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>{t('Name')}</Th>,
<Th key={'aliases'} w='x200'>{t('Aliases')}</Th>,
<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) => {
@ -40,5 +28,15 @@ export function CustomEmoji({
</Table.Row>;
};
return <GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data.emojis} total={data.total} setParams={setParams} params={params} />;
return <GenericTable
header={header}
renderRow={renderRow}
results={data.emojis}
total={data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
/>;
}
export default CustomEmoji;

@ -6,12 +6,12 @@ import { usePermission } from '../../contexts/AuthorizationContext';
import { useTranslation } from '../../contexts/TranslationContext';
import Page from '../../components/basic/Page';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { CustomEmoji } from './CustomEmoji';
import { EditCustomEmojiWithData } from './EditCustomEmoji';
import { AddCustomEmoji } from './AddCustomEmoji';
import AddCustomEmoji from './AddCustomEmoji';
import CustomEmoji from './CustomEmoji';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useEndpointData } from '../../hooks/useEndpointData';
import VerticalBar from '../../components/basic/VerticalBar';
import EditCustomEmojiWithData from './EditCustomEmojiWithData';
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
@ -23,7 +23,7 @@ export const useQuery = ({ text, itemsPerPage, current }, [column, direction], c
// TODO: remove cache. Is necessary for data invalidation
}), [text, itemsPerPage, current, column, direction, cache]);
export default function CustomEmojiRoute({ props }) {
function CustomEmojiRoute({ props }) {
const t = useTranslation();
const canManageEmoji = usePermission('manage-emoji');
@ -101,3 +101,6 @@ export default function CustomEmojiRoute({ props }) {
</VerticalBar>}
</Page>;
}
export default CustomEmojiRoute;

@ -1,86 +1,24 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon, Skeleton, Throbber, InputBox, Modal } from '@rocket.chat/fuselage';
import React, { useCallback, useState, useMemo, useEffect, FC, ChangeEvent } from 'react';
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFileInput } from '../../hooks/useFileInput';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import { useEndpointUpload } from '../../hooks/useEndpointUpload';
import { useSetModal } from '../../contexts/ModalContext';
import { useEndpointAction } from '../../hooks/useEndpointAction';
import VerticalBar from '../../components/basic/VerticalBar';
import DeleteSuccessModal from '../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../components/DeleteWarningModal';
import { EmojiDescriptor } from './types';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Emoji_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Emoji_Has_Been_Deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
type EditCustomEmojiProps = {
close: () => void;
onChange: () => void;
data: EmojiDescriptor;
};
export function EditCustomEmojiWithData({ _id, cache, onChange, ...props }) {
const t = useTranslation();
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
// TODO: remove cache. Is necessary for data invalidation
}), [_id, cache]);
const { data = { emojis: {} }, state, error } = useEndpointDataExperimental('emoji-custom.list', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || !data.emojis || data.emojis.update.length < 1) {
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>;
}
return <EditCustomEmoji data={data.emojis.update[0]} onChange={onChange} {...props}/>;
}
export function EditCustomEmoji({ close, onChange, data, ...props }) {
const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...props }) => {
const t = useTranslation();
const { _id, name: previousName, aliases: previousAliases, extension: previousExtension } = data || {};
@ -88,7 +26,7 @@ export function EditCustomEmoji({ close, onChange, data, ...props }) {
const [name, setName] = useState(previousName);
const [aliases, setAliases] = useState(previousAliases.join(', '));
const [emojiFile, setEmojiFile] = useState();
const [emojiFile, setEmojiFile] = useState<Blob>();
const setModal = useSetModal();
const [newEmojiPreview, setNewEmojiPreview] = useState(`/emoji-custom/${ encodeURIComponent(previousName) }.${ previousExtension }`);
@ -107,12 +45,16 @@ export function EditCustomEmoji({ close, onChange, data, ...props }) {
const saveAction = useEndpointUpload('emoji-custom.update', {}, t('Custom_Emoji_Updated_Successfully'));
const handleSave = useCallback(async () => {
if (!emojiFile) {
return;
}
const formData = new FormData();
formData.append('emoji', emojiFile);
formData.append('_id', _id);
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();
}
@ -123,21 +65,28 @@ export function EditCustomEmoji({ close, onChange, data, ...props }) {
const onDeleteConfirm = useCallback(async () => {
const result = await deleteAction();
if (result.success) {
setModal(() => <SuccessModal onClose={() => { setModal(undefined); close(); onChange(); }}/>);
setModal(() => <DeleteSuccessModal
children={t('Custom_Emoji_Has_Been_Deleted')}
onClose={(): void => { setModal(undefined); close(); onChange(); }}
/>);
}
}, [close, deleteAction, onChange]);
}, [close, deleteAction, onChange, setModal, t]);
const openConfirmDelete = useCallback(() => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>), [onDeleteConfirm, setModal]);
const openConfirmDelete = useCallback(() => setModal(() => <DeleteWarningModal
children={t('Custom_Emoji_Delete_Warning')}
onDelete={onDeleteConfirm}
onCancel={(): void => setModal(undefined)}
/>), [onDeleteConfirm, setModal, t]);
const handleAliasesChange = useCallback((e) => setAliases(e.currentTarget.value), [setAliases]);
const [clickUpload] = useFileInput(setEmojiPreview, 'emoji');
return <VerticalBar.ScrollableContent {...props}>
return <VerticalBar.ScrollableContent {...(props as any)}>
<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={(e: ChangeEvent<HTMLInputElement>): void => setName(e.currentTarget.value)} placeholder={t('Name')} />
</Field.Row>
</Field>
<Field>
@ -173,4 +122,6 @@ export function EditCustomEmoji({ close, onChange, data, ...props }) {
</Field.Row>
</Field>
</VerticalBar.ScrollableContent>;
}
};
export default EditCustomEmoji;

@ -0,0 +1,60 @@
import React, { useMemo, FC } from 'react';
import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import EditCustomEmoji from './EditCustomEmoji';
import { EmojiDescriptor } from './types';
type EditCustomEmojiWithDataProps = {
_id: string;
cache: unknown;
close: () => void;
onChange: () => void;
};
const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, cache, onChange, ...props }) => {
const t = useTranslation();
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
// TODO: remove cache. Is necessary for data invalidation
}), [_id, cache]);
const {
data = {
emojis: {
update: [],
},
},
state,
error,
} = useEndpointDataExperimental<{
emojis?: {
update: EmojiDescriptor[];
};
}>('emoji-custom.list', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || !data.emojis || data.emojis.update.length < 1) {
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>;
}
return <EditCustomEmoji data={data.emojis.update[0]} onChange={onChange} {...props}/>;
};
export default EditCustomEmojiWithData;

@ -0,0 +1,6 @@
export type EmojiDescriptor = {
_id: string;
name: string;
aliases: string[];
extension: string;
};

@ -8,7 +8,7 @@ import { useFileInput } from '../../hooks/useFileInput';
import { validate, createSoundData } from './lib';
import VerticalBar from '../../components/basic/VerticalBar';
export function AddCustomSound({ goToNew, close, onChange, ...props }) {
function AddCustomSound({ goToNew, close, onChange, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
@ -98,3 +98,5 @@ export function AddCustomSound({ goToNew, close, onChange, ...props }) {
</Field>
</VerticalBar.ScrollableContent>;
}
export default AddCustomSound;

@ -1,24 +1,12 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { Box, Table, TextInput, Icon, Button } from '@rocket.chat/fuselage';
import React, { useMemo, useCallback } from 'react';
import { Box, Table, Icon, Button } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { GenericTable, Th } from '../../components/GenericTable';
import FilterByText from '../../components/FilterByText';
import GenericTable from '../../components/GenericTable';
import { useCustomSound } from '../../contexts/CustomSoundContext';
import { useTranslation } from '../../contexts/TranslationContext';
const FilterByText = ({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput flexShrink={0} placeholder={t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
};
export function AdminSounds({
function AdminSounds({
data,
sort,
onClick,
@ -29,8 +17,8 @@ export function AdminSounds({
const t = useTranslation();
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</Th>,
<Th w='x40' key='action'></Th>,
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell w='x40' key='action' />,
], [sort]);
const customSound = useCustomSound();
@ -52,5 +40,15 @@ export function AdminSounds({
</Table.Row>;
};
return <GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data.sounds} total={data.total} setParams={setParams} params={params} />;
return <GenericTable
header={header}
renderRow={renderRow}
results={data.sounds}
total={data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
/>;
}
export default AdminSounds;

@ -7,9 +7,9 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useTranslation } from '../../contexts/TranslationContext';
import Page from '../../components/basic/Page';
import { AdminSounds } from './AdminSounds';
import { AddCustomSound } from './AddCustomSound';
import { EditCustomSound } from './EditCustomSound';
import AdminSounds from './AdminSounds';
import AddCustomSound from './AddCustomSound';
import EditCustomSound from './EditCustomSound';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useEndpointData } from '../../hooks/useEndpointData';
import VerticalBar from '../../components/basic/VerticalBar';

@ -1,5 +1,5 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon, Skeleton, Throbber, InputBox, Modal } from '@rocket.chat/fuselage';
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
@ -9,47 +9,10 @@ import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEnd
import { validate, createSoundData } from './lib';
import { useSetModal } from '../../contexts/ModalContext';
import VerticalBar from '../../components/basic/VerticalBar';
import DeleteSuccessModal from '../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../components/DeleteWarningModal';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Sound_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Sound_Has_Been_Deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export function EditCustomSound({ _id, cache, ...props }) {
function EditCustomSound({ _id, cache, ...props }) {
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
}), [_id]);
@ -146,14 +109,21 @@ function EditSound({ close, onChange, data, ...props }) {
const onDeleteConfirm = useCallback(async () => {
try {
await deleteCustomSound(_id);
setModal(() => <SuccessModal onClose={() => { setModal(undefined); close(); onChange(); }}/>);
setModal(() => <DeleteSuccessModal
children={t('Custom_Sound_Has_Been_Deleted')}
onClose={() => { setModal(undefined); close(); onChange(); }}
/>);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
onChange();
}
}, [_id, close, deleteCustomSound, dispatchToastMessage, onChange]);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal
children={t('Custom_Sound_Delete_Warning')}
onDelete={onDeleteConfirm}
onCancel={() => setModal(undefined)}
/>);
const [clickUpload] = useFileInput(handleChangeFile, 'audio/mp3');
@ -192,3 +162,5 @@ function EditSound({ close, onChange, data, ...props }) {
</Field>
</VerticalBar.ScrollableContent>;
}
export default EditCustomSound;

@ -6,7 +6,7 @@ import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import VerticalBar from '../../components/basic/VerticalBar';
export function AddCustomUserStatus({ goToNew, close, onChange, ...props }) {
function AddCustomUserStatus({ goToNew, close, onChange, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
@ -58,3 +58,5 @@ export function AddCustomUserStatus({ goToNew, close, onChange, ...props }) {
</Field>
</VerticalBar.ScrollableContent>;
}
export default AddCustomUserStatus;

@ -1,25 +1,13 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import { Table } from '@rocket.chat/fuselage';
import { GenericTable, Th } from '../../components/GenericTable';
import FilterByText from '../../components/FilterByText';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' };
const FilterByText = ({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput flexShrink={0} placeholder={t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
};
export function CustomUserStatus({
function CustomUserStatus({
data,
sort,
onClick,
@ -30,8 +18,8 @@ export function CustomUserStatus({
const t = useTranslation();
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</Th>,
<Th key={'presence'} direction={sort[1]} active={sort[0] === 'statusType'} onClick={onHeaderClick} sort='statusType'>{t('Presence')}</Th>,
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'presence'} direction={sort[1]} active={sort[0] === 'statusType'} onClick={onHeaderClick} sort='statusType'>{t('Presence')}</GenericTable.HeaderCell>,
].filter(Boolean), [onHeaderClick, sort, t]);
const renderRow = (status) => {
@ -42,5 +30,15 @@ export function CustomUserStatus({
</Table.Row>;
};
return <GenericTable FilterComponent={FilterByText} header={header} renderRow={renderRow} results={data.statuses} total={data.total} setParams={setParams} params={params} />;
return <GenericTable
header={header}
renderRow={renderRow}
results={data.statuses}
total={data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
/>;
}
export default CustomUserStatus;

@ -5,13 +5,13 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useTranslation } from '../../contexts/TranslationContext';
import Page from '../../components/basic/Page';
import { CustomUserStatus } from './CustomUserStatus';
import { EditCustomUserStatusWithData } from './EditCustomUserStatus';
import { AddCustomUserStatus } from './AddCustomUserStatus';
import CustomUserStatus from './CustomUserStatus';
import AddCustomUserStatus from './AddCustomUserStatus';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useEndpointData } from '../../hooks/useEndpointData';
import VerticalBar from '../../components/basic/VerticalBar';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import EditCustomUserStatusWithData from './EditCustomUserStatusWithData';
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
@ -23,7 +23,7 @@ export const useQuery = ({ text, itemsPerPage, current }, [column, direction], c
// TODO: remove cache. Is necessary for data invalidation
}), [text, itemsPerPage, current, column, direction, cache]);
export default function CustomUserStatusRoute({ props }) {
function CustomUserStatusRoute({ props }) {
const t = useTranslation();
const canManageUserStatus = usePermission('manage-user-status');
@ -102,3 +102,5 @@ export default function CustomUserStatusRoute({ props }) {
</VerticalBar>}
</Page>;
}
export default CustomUserStatusRoute;

@ -1,83 +1,13 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { Box, Button, ButtonGroup, TextInput, Field, Select, Icon, Skeleton, Throbber, InputBox, Modal } from '@rocket.chat/fuselage';
import { Button, ButtonGroup, TextInput, Field, Select, Icon } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useSetModal } from '../../contexts/ModalContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import VerticalBar from '../../components/basic/VerticalBar';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_User_Status_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_User_Status_Has_Been_Deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export function EditCustomUserStatusWithData({ _id, cache, ...props }) {
const t = useTranslation();
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
// TODO: remove cache. Is necessary for data invalidation
}), [_id, cache]);
const { data, state, error } = useEndpointDataExperimental('custom-user-status.list', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || data.statuses.length < 1) {
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>;
}
return <EditCustomUserStatus data={data.statuses[0]} {...props}/>;
}
import DeleteSuccessModal from '../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../components/DeleteWarningModal';
export function EditCustomUserStatus({ close, onChange, data, ...props }) {
const t = useTranslation();
@ -117,14 +47,21 @@ export function EditCustomUserStatus({ close, onChange, data, ...props }) {
const onDeleteConfirm = useCallback(async () => {
try {
await deleteStatus(_id);
setModal(() => <SuccessModal onClose={() => { setModal(undefined); close(); onChange(); }}/>);
setModal(() => <DeleteSuccessModal
children={t('Custom_User_Status_Has_Been_Deleted')}
onClose={() => { setModal(undefined); close(); onChange(); }}
/>);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
onChange();
}
}, [_id, close, deleteStatus, dispatchToastMessage, onChange]);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal
children={t('Custom_User_Status_Delete_Warning')}
onDelete={onDeleteConfirm}
onCancel={() => setModal(undefined)}
/>);
const presenceOptions = [
['online', t('Online')],
@ -163,3 +100,5 @@ export function EditCustomUserStatus({ close, onChange, data, ...props }) {
</Field>
</VerticalBar.ScrollableContent>;
}
export default EditCustomUserStatus;

@ -0,0 +1,49 @@
import React, { useMemo, FC } from 'react';
import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import EditCustomUserStatus from './EditCustomUserStatus';
type EditCustomUserStatusWithDataProps = {
_id: string;
cache: unknown;
close: () => void;
onChange: () => void;
};
export const EditCustomUserStatusWithData: FC<EditCustomUserStatusWithDataProps> = ({ _id, cache, ...props }) => {
const t = useTranslation();
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
// TODO: remove cache. Is necessary for data invalidation
}), [_id, cache]);
const { data, state, error } = useEndpointDataExperimental<{
statuses: unknown[];
}>('custom-user-status.list', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || data.statuses.length < 1) {
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>;
}
return <EditCustomUserStatus data={data.statuses[0]} {...props}/>;
};
export default EditCustomUserStatusWithData;

@ -4,7 +4,7 @@ import { useRole } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import FederationDashboardPage from './FederationDashboardPage';
const FederationDashboardRoute: FC<{}> = () => {
const FederationDashboardRoute: FC = () => {
const authorized = useRole('admin');
if (!authorized) {

@ -1,5 +1,5 @@
import { Box, Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import React, { useMemo } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import CounterSet from '../../components/data/CounterSet';
@ -7,7 +7,7 @@ import { usePolledMethodData, AsyncState } from '../../contexts/ServerContext';
function OverviewSection() {
const t = useTranslation();
const [overviewData, overviewStatus] = usePolledMethodData('federation:getOverviewData', [], 10000);
const [overviewData, overviewStatus] = usePolledMethodData('federation:getOverviewData', useMemo(() => [], []), 10000);
const eventCount = (overviewStatus === AsyncState.LOADING && <Skeleton variant='text' />)
|| (overviewStatus === AsyncState.ERROR && <Box color='danger'>Error</Box>)

@ -1,10 +1,10 @@
import { Box, Throbber } from '@rocket.chat/fuselage';
import React from 'react';
import React, { useMemo } from 'react';
import { usePolledMethodData, AsyncState } from '../../contexts/ServerContext';
function ServersSection() {
const [serversData, serversStatus] = usePolledMethodData('federation:getServers', [], 10000);
const [serversData, serversStatus] = usePolledMethodData('federation:getServers', useMemo(() => [], []), 10000);
if (serversStatus === AsyncState.LOADING) {
return <Throbber align='center' />;

@ -0,0 +1,95 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
CheckBox,
Table,
Tag,
Pagination,
} from '@rocket.chat/fuselage';
import React, { useState, useCallback, FC, Dispatch, SetStateAction, ChangeEvent } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
type ChannelDescriptor = {
channel_id: string;
name: string;
is_archived: boolean;
do_import: boolean;
};
type PrepareChannelsProps = {
channelsCount: number;
channels: ChannelDescriptor[];
setChannels: Dispatch<SetStateAction<ChannelDescriptor[]>>;
};
const PrepareChannels: FC<PrepareChannelsProps> = ({ channels, channelsCount, setChannels }) => {
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25);
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]);
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
if (!channels.length) {
return null;
}
return <>
<Table>
<Table.Head>
<Table.Row>
<Table.Cell width='x36'>
<CheckBox
checked={channelsCount > 0}
indeterminate={channelsCount > 0 && channelsCount !== channels.length}
onChange={(): void => {
setChannels((channels) => {
const hasCheckedArchivedChannels = channels.some(({ is_archived, do_import }) => is_archived && do_import);
const isChecking = channelsCount === 0;
if (isChecking) {
return channels.map((channel) => ({ ...channel, do_import: true }));
}
if (hasCheckedArchivedChannels) {
return channels.map((channel) => (channel.is_archived ? { ...channel, do_import: false } : channel));
}
return channels.map((channel) => ({ ...channel, do_import: false }));
});
}}
/>
</Table.Cell>
<Table.Cell is='th'>{t('Name')}</Table.Cell>
<Table.Cell is='th' align='end'></Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{channels.slice(current, current + itemsPerPage).map((channel) => <Table.Row key={channel.channel_id}>
<Table.Cell width='x36'>
<CheckBox
checked={channel.do_import}
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
const { checked } = event.currentTarget;
setChannels((channels) =>
channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel)));
}}
/>
</Table.Cell>
<Table.Cell>{channel.name}</Table.Cell>
<Table.Cell align='end'>{channel.is_archived && <Tag variant='danger'>{t('Importer_Archived')}</Tag>}</Table.Cell>
</Table.Row>)}
</Table.Body>
</Table>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
count={channels.length || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</>;
};
export default PrepareChannels;

@ -3,17 +3,13 @@ import {
Box,
Button,
ButtonGroup,
CheckBox,
Icon,
Margins,
Table,
Tag,
Throbber,
Pagination,
Tabs,
} from '@rocket.chat/fuselage';
import { useDebouncedValue, useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
@ -30,6 +26,8 @@ import {
import { useErrorHandler } from './useErrorHandler';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpoint } from '../../contexts/ServerContext';
import PrepareUsers from './PrepareUsers';
import PrepareChannels from './PrepareChannels';
const waitFor = (fn, predicate) => new Promise((resolve, reject) => {
const callPromise = () => {
@ -46,138 +44,6 @@ const waitFor = (fn, predicate) => new Promise((resolve, reject) => {
callPromise();
});
function PrepareUsers({ usersCount, users, setUsers }) {
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(25);
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]);
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
return <>
<Table>
<Table.Head>
<Table.Row>
<Table.Cell width='x36'>
<CheckBox
checked={usersCount > 0}
indeterminate={usersCount > 0 && usersCount !== users.length}
onChange={() => {
setUsers((users) => {
const hasCheckedDeletedUsers = users.some(({ is_deleted, do_import }) => is_deleted && do_import);
const isChecking = usersCount === 0;
if (isChecking) {
return users.map((user) => ({ ...user, do_import: true }));
}
if (hasCheckedDeletedUsers) {
return users.map((user) => (user.is_deleted ? { ...user, do_import: false } : user));
}
return users.map((user) => ({ ...user, do_import: false }));
});
}}
/>
</Table.Cell>
<Table.Cell is='th'>{t('Username')}</Table.Cell>
<Table.Cell is='th'>{t('Email')}</Table.Cell>
<Table.Cell is='th'></Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{users.slice(current, current + itemsPerPage).map((user) => <Table.Row key={user.user_id}>
<Table.Cell width='x36'>
<CheckBox
checked={user.do_import}
onChange={(event) => {
const { checked } = event.currentTarget;
setUsers((users) =>
users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user)));
}}
/>
</Table.Cell>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
<Table.Cell align='end'>{user.is_deleted && <Tag variant='danger'>{t('Deleted')}</Tag>}</Table.Cell>
</Table.Row>)}
</Table.Body>
</Table>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
count={users.length || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
/>
</>;
}
function PrepareChannels({ channels, channelsCount, setChannels }) {
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(25);
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]);
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
return channels.length && <><Table>
<Table.Head>
<Table.Row>
<Table.Cell width='x36'>
<CheckBox
checked={channelsCount > 0}
indeterminate={channelsCount > 0 && channelsCount !== channels.length}
onChange={() => {
setChannels((channels) => {
const hasCheckedArchivedChannels = channels.some(({ is_archived, do_import }) => is_archived && do_import);
const isChecking = channelsCount === 0;
if (isChecking) {
return channels.map((channel) => ({ ...channel, do_import: true }));
}
if (hasCheckedArchivedChannels) {
return channels.map((channel) => (channel.is_archived ? { ...channel, do_import: false } : channel));
}
return channels.map((channel) => ({ ...channel, do_import: false }));
});
}}
/>
</Table.Cell>
<Table.Cell is='th'>{t('Name')}</Table.Cell>
<Table.Cell is='th' align='end'></Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{channels.slice(current, current + itemsPerPage).map((channel) => <Table.Row key={channel.channel_id}>
<Table.Cell width='x36'>
<CheckBox
checked={channel.do_import}
onChange={(event) => {
const { checked } = event.currentTarget;
setChannels((channels) =>
channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel)));
}}
/>
</Table.Cell>
<Table.Cell>{channel.name}</Table.Cell>
<Table.Cell align='end'>{channel.is_archived && <Tag variant='danger'>{t('Importer_Archived')}</Tag>}</Table.Cell>
</Table.Row>)}
</Table.Body>
</Table>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
count={channels.length || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/></>;
}
function PrepareImportPage() {
const t = useTranslation();
const handleError = useErrorHandler();

@ -0,0 +1,94 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
CheckBox,
Table,
Tag,
Pagination,
} from '@rocket.chat/fuselage';
import React, { useState, useCallback, FC, Dispatch, SetStateAction, ChangeEvent } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
type UserDescriptor = {
user_id: string;
username: string;
email: string;
is_deleted: boolean;
do_import: boolean;
};
type PrepareUsersProps = {
usersCount: number;
users: UserDescriptor[];
setUsers: Dispatch<SetStateAction<UserDescriptor[]>>;
};
const PrepareUsers: FC<PrepareUsersProps> = ({ usersCount, users, setUsers }) => {
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25);
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]);
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
return <>
<Table>
<Table.Head>
<Table.Row>
<Table.Cell width='x36'>
<CheckBox
checked={usersCount > 0}
indeterminate={usersCount > 0 && usersCount !== users.length}
onChange={(): void => {
setUsers((users) => {
const hasCheckedDeletedUsers = users.some(({ is_deleted, do_import }) => is_deleted && do_import);
const isChecking = usersCount === 0;
if (isChecking) {
return users.map((user) => ({ ...user, do_import: true }));
}
if (hasCheckedDeletedUsers) {
return users.map((user) => (user.is_deleted ? { ...user, do_import: false } : user));
}
return users.map((user) => ({ ...user, do_import: false }));
});
}}
/>
</Table.Cell>
<Table.Cell is='th'>{t('Username')}</Table.Cell>
<Table.Cell is='th'>{t('Email')}</Table.Cell>
<Table.Cell is='th'></Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{users.slice(current, current + itemsPerPage).map((user) => <Table.Row key={user.user_id}>
<Table.Cell width='x36'>
<CheckBox
checked={user.do_import}
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
const { checked } = event.currentTarget;
setUsers((users) =>
users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user)));
}}
/>
</Table.Cell>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
<Table.Cell align='end'>{user.is_deleted && <Tag variant='danger'>{t('Deleted')}</Tag>}</Table.Cell>
</Table.Row>)}
</Table.Body>
</Table>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
count={users.length || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
/>
</>;
};
export default PrepareUsers;

@ -3,9 +3,9 @@ import React from 'react';
import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
export const BuildEnvironmentSection = React.memo(function BuildEnvironmentSection({ info }) {
const BuildEnvironmentSection = React.memo(function BuildEnvironmentSection({ info }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const build = info && (info.compile || info.build);
@ -21,3 +21,5 @@ export const BuildEnvironmentSection = React.memo(function BuildEnvironmentSecti
<DescriptionList.Entry label={t('Date')}>{formatDateAndTime(build.date)}</DescriptionList.Entry>
</DescriptionList>;
});
export default BuildEnvironmentSection;

@ -1,7 +1,7 @@
import React from 'react';
import { dummyDate } from '../../../.storybook/helpers';
import { BuildEnvironmentSection } from './BuildEnvironmentSection';
import BuildEnvironmentSection from './BuildEnvironmentSection';
export default {
title: 'admin/info/BuildEnvironmentSection',

@ -2,9 +2,9 @@ import React from 'react';
import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
export const CommitSection = React.memo(function CommitSection({ info }) {
const CommitSection = React.memo(function CommitSection({ info }) {
const t = useTranslation();
const { commit = {} } = info;
@ -20,3 +20,5 @@ export const CommitSection = React.memo(function CommitSection({ info }) {
<DescriptionList.Entry label={t('Subject')}>{commit.subject}</DescriptionList.Entry>
</DescriptionList>;
});
export default CommitSection;

@ -1,6 +1,6 @@
import React from 'react';
import { CommitSection } from './CommitSection';
import CommitSection from './CommitSection';
export default {
title: 'admin/info/CommitSection',

@ -3,7 +3,7 @@ import React from 'react';
const style = { wordBreak: 'break-word' };
export const DescriptionList = React.memo(({ children, title, ...props }) => <>
const DescriptionList = React.memo(({ children, title, ...props }) => <>
{title && <Box display='flex' justifyContent='flex-end' width='30%' paddingInline='x8'>
{title}
</Box>}
@ -14,10 +14,12 @@ export const DescriptionList = React.memo(({ children, title, ...props }) => <>
</Table>
</>);
const Entry = ({ children, label, ...props }) =>
const DescriptionListEntry = ({ children, label, ...props }) =>
<Table.Row {...props}>
<Table.Cell is='th' scope='col' width='30%' align='end' color='hint' backgroundColor='surface' fontScale='p2' style={style}>{label}</Table.Cell>
<Table.Cell width='70%' align='start' color='default' style={style}>{children}</Table.Cell>
</Table.Row>;
DescriptionList.Entry = React.memo(Entry);
DescriptionList.Entry = React.memo(DescriptionListEntry);
export default DescriptionList;

@ -1,6 +1,6 @@
import React from 'react';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
import Page from '../../components/basic/Page';
export default {

@ -3,14 +3,14 @@ import React from 'react';
import Page from '../../components/basic/Page';
import { useTranslation } from '../../contexts/TranslationContext';
import { RocketChatSection } from './RocketChatSection';
import { CommitSection } from './CommitSection';
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection';
import { BuildEnvironmentSection } from './BuildEnvironmentSection';
import { UsageSection } from './UsageSection';
import { InstancesSection } from './InstancesSection';
import RocketChatSection from './RocketChatSection';
import CommitSection from './CommitSection';
import RuntimeEnvironmentSection from './RuntimeEnvironmentSection';
import BuildEnvironmentSection from './BuildEnvironmentSection';
import UsageSection from './UsageSection';
import InstancesSection from './InstancesSection';
export const InformationPage = React.memo(function InformationPage({
const InformationPage = React.memo(function InformationPage({
canViewStatistics,
isLoading,
info,
@ -73,3 +73,5 @@ export const InformationPage = React.memo(function InformationPage({
</Page.ScrollableContentWithShadow>
</Page>;
});
export default InformationPage;

@ -3,7 +3,7 @@ import { boolean, object } from '@storybook/addon-knobs/react';
import React from 'react';
import { dummyDate } from '../../../.storybook/helpers';
import { InformationPage } from './InformationPage';
import InformationPage from './InformationPage';
export default {
title: 'admin/info/InformationPage',

@ -4,12 +4,11 @@ import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { useMethod, useServerInformation, useEndpoint } from '../../contexts/ServerContext';
import { downloadJsonAsAFile } from '../../helpers/download';
import { InformationPage } from './InformationPage';
import InformationPage from './InformationPage';
export const InformationRoute = React.memo(function InformationRoute() {
const InformationRoute = React.memo(function InformationRoute() {
const canViewStatistics = usePermission('view-statistics');
const [isLoading, setLoading] = useState(true);
const [statistics, setStatistics] = useState({});
const [instances, setInstances] = useState([]);

@ -3,9 +3,9 @@ import React from 'react';
import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
export function InstancesSection({ instances }) {
function InstancesSection({ instances }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
@ -29,3 +29,5 @@ export function InstancesSection({ instances }) {
)}
</>;
}
export default InstancesSection;

@ -1,7 +1,7 @@
import React from 'react';
import { dummyDate } from '../../../.storybook/helpers';
import { InstancesSection } from './InstancesSection';
import InstancesSection from './InstancesSection';
export default {
title: 'admin/info/InstancesSection',

@ -5,9 +5,9 @@ import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
import { useFormatDuration } from '../../hooks/useFormatDuration';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
export const RocketChatSection = React.memo(function RocketChatSection({ info, statistics, isLoading }) {
const RocketChatSection = React.memo(function RocketChatSection({ info, statistics, isLoading }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const formatDuration = useFormatDuration();
@ -32,3 +32,5 @@ export const RocketChatSection = React.memo(function RocketChatSection({ info, s
<DescriptionList.Entry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</DescriptionList.Entry>
</DescriptionList>;
});
export default RocketChatSection;

@ -1,7 +1,7 @@
import React from 'react';
import { dummyDate } from '../../../.storybook/helpers';
import { RocketChatSection } from './RocketChatSection';
import RocketChatSection from './RocketChatSection';
export default {
title: 'admin/info/RocketChatSection',

@ -6,7 +6,7 @@ import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatMemorySize } from '../../hooks/useFormatMemorySize';
import { useFormatDuration } from '../../hooks/useFormatDuration';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
const formatCPULoad = (load) => {
if (!load) {
@ -17,7 +17,7 @@ const formatCPULoad = (load) => {
return `${ s.numberFormat(oneMinute, 2) }, ${ s.numberFormat(fiveMinutes, 2) }, ${ s.numberFormat(fifteenMinutes, 2) }`;
};
export const RuntimeEnvironmentSection = React.memo(function RuntimeEnvironmentSection({ statistics, isLoading }) {
const RuntimeEnvironmentSection = React.memo(function RuntimeEnvironmentSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const t = useTranslation();
const formatMemorySize = useFormatMemorySize();
@ -41,3 +41,5 @@ export const RuntimeEnvironmentSection = React.memo(function RuntimeEnvironmentS
<DescriptionList.Entry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</DescriptionList.Entry>
</DescriptionList>;
});
export default RuntimeEnvironmentSection;

@ -1,6 +1,6 @@
import React from 'react';
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection';
import RuntimeEnvironmentSection from './RuntimeEnvironmentSection';
export default {
title: 'admin/info/RuntimeEnvironmentSection',

@ -4,9 +4,9 @@ import React from 'react';
import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useFormatMemorySize } from '../../hooks/useFormatMemorySize';
import { DescriptionList } from './DescriptionList';
import DescriptionList from './DescriptionList';
export const UsageSection = React.memo(function UsageSection({ statistics, isLoading }) {
const UsageSection = React.memo(function UsageSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const formatMemorySize = useFormatMemorySize();
const t = useTranslation();
@ -50,3 +50,5 @@ export const UsageSection = React.memo(function UsageSection({ statistics, isLoa
<DescriptionList.Entry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</DescriptionList.Entry>
</DescriptionList>;
});
export default UsageSection;

@ -1,6 +1,6 @@
import React from 'react';
import { UsageSection } from './UsageSection';
import UsageSection from './UsageSection';
export default {
title: 'admin/info/UsageSection',

@ -2,7 +2,7 @@ import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage';
import { useDebouncedValue, useResizeObserver } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { GenericTable, Th } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
@ -95,16 +95,25 @@ export function IntegrationsTable({ type }) {
}, [sort]);
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w={isBig ? 'x280' : 'x240'}>{t('Name')}</Th>,
<Th key={'channel'} direction={sort[1]} active={sort[0] === 'channel'} onClick={onHeaderClick} sort='channel'>{t('Post_to')}</Th>,
<Th key={'_createdBy'} direction={sort[1]} active={sort[0] === '_createdBy'} onClick={onHeaderClick} sort='_createdBy'>{t('Created_by')}</Th>,
isBig && <Th key={'_createdAt'} direction={sort[1]} active={sort[0] === '_createdAt'} onClick={onHeaderClick} sort='_createdAt'>{t('Created_at')}</Th>,
<Th key={'username'} direction={sort[1]} active={sort[0] === 'username'} onClick={onHeaderClick} sort='username'>{t('Post_as')}</Th>,
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w={isBig ? 'x280' : 'x240'}>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'channel'} direction={sort[1]} active={sort[0] === 'channel'} onClick={onHeaderClick} sort='channel'>{t('Post_to')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'_createdBy'} direction={sort[1]} active={sort[0] === '_createdBy'} onClick={onHeaderClick} sort='_createdBy'>{t('Created_by')}</GenericTable.HeaderCell>,
isBig && <GenericTable.HeaderCell key={'_createdAt'} direction={sort[1]} active={sort[0] === '_createdAt'} onClick={onHeaderClick} sort='_createdAt'>{t('Created_at')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'username'} direction={sort[1]} active={sort[0] === 'username'} onClick={onHeaderClick} sort='username'>{t('Post_as')}</GenericTable.HeaderCell>,
].filter(Boolean), [sort, onHeaderClick, isBig, t]);
const renderRow = useCallback((props) => <IntegrationRow {...props} isBig={isBig} onClick={onClick} />, [isBig, onClick]);
return <GenericTable ref={ref} FilterComponent={FilterByTypeAndText} header={header} renderRow={renderRow} results={data && data.integrations} total={data && data.total} setParams={setParams} params={params} />;
return <GenericTable
ref={ref}
header={header}
renderRow={renderRow}
results={data && data.integrations}
total={data && data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByTypeAndText setFilter={onChange} {...props} />}
/>;
}
export default IntegrationsTable;

@ -1,7 +1,6 @@
import React, { useMemo, useState, useCallback } from 'react';
import { Field, Box, Skeleton, Margins, Button } from '@rocket.chat/fuselage';
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { useMethod } from '../../../contexts/ServerContext';
@ -11,6 +10,8 @@ import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'
import { useSetModal } from '../../../contexts/ModalContext';
import { useForm } from '../../../hooks/useForm';
import IncomingWebhookForm from '../IncomingWebhookForm';
import DeleteSuccessModal from '../../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../../components/DeleteWarningModal';
export default function EditIncomingWebhookWithData({ integrationId, ...props }) {
const t = useTranslation();
@ -71,11 +72,20 @@ function EditIncomingWebhook({ data, onChange, ...props }) {
const closeModal = () => setModal();
const onDelete = async () => {
const result = await deleteIntegration();
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); }
if (result.success) {
setModal(<DeleteSuccessModal
children={t('Your_entry_has_been_deleted')}
onClose={() => { closeModal(); router.push({}); }}
/>);
}
};
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />);
}, [deleteIntegration, router]);
setModal(<DeleteWarningModal
children={t('Integration_Delete_Warning')}
onDelete={onDelete}
onCancel={closeModal}
/>);
}, [deleteIntegration, router, setModal, t]);
const handleSave = useCallback(async () => {
try {

@ -1,4 +1,4 @@
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage';
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage';
import React, { useCallback } from 'react';
import Page from '../../../components/basic/Page';
@ -7,45 +7,6 @@ import EditOutgoingWebhookWithData from './EditOutgoingWebhook';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext';
export const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Integration_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Your_entry_has_been_deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default function EditIntegrationsPage({ ...props }) {
const t = useTranslation();

@ -7,7 +7,6 @@ import {
Button,
} from '@rocket.chat/fuselage';
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
@ -17,6 +16,8 @@ import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'
import { useSetModal } from '../../../contexts/ModalContext';
import OutgoingWebhookForm from '../OutgoiongWebhookForm';
import { useForm } from '../../../hooks/useForm';
import DeleteSuccessModal from '../../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../../components/DeleteWarningModal';
export default function EditOutgoingWebhookWithData({ integrationId, ...props }) {
const t = useTranslation();
@ -89,11 +90,20 @@ function EditOutgoingWebhook({ data, onChange, setSaveAction, ...props }) {
const closeModal = () => setModal();
const onDelete = async () => {
const result = await deleteIntegration();
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); }
if (result.success) {
setModal(<DeleteSuccessModal
children={t('Your_entry_has_been_deleted')}
onClose={() => { closeModal(); router.push({}); }}
/>);
}
};
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />);
}, [deleteIntegration, router]);
setModal(<DeleteWarningModal
children={t('Integration_Delete_Warning')}
onDelete={onDelete}
onCancel={closeModal}
/>);
}, [deleteIntegration, router, setModal, t]);
const {
urls,

@ -13,7 +13,7 @@ import { useModal } from '../../contexts/ModalContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpoint } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { GenericTable } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';

@ -1,31 +1,46 @@
import React from 'react';
import toastr from 'toastr';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { Mailer } from './Mailer';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
const useSendMail = () => {
const meteorSendMail = useMethod('Mailer.sendMail');
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
return ({ fromEmail, subject, emailBody, dryRun, query }) => {
if (query.error) {
toastr.error(t('Query_is_not_valid_JSON'));
dispatchToastMessage({
type: 'error',
message: t('Query_is_not_valid_JSON'),
});
return;
}
if (fromEmail.error || fromEmail.length < 1) {
toastr.error(t('error-invalid-from-address'));
dispatchToastMessage({
type: 'error',
message: t('error-invalid-from-address'),
});
return;
}
if (emailBody.indexOf('[unsubscribe]') === -1) {
toastr.error(t('error-missing-unsubscribe-link'));
dispatchToastMessage({
type: 'error',
message: t('error-missing-unsubscribe-link'),
});
return;
}
meteorSendMail(fromEmail.value, subject, emailBody, dryRun, query.value);
toastr.success(t('The_emails_are_being_sent'));
dispatchToastMessage({
type: 'success',
message: t('The_emails_are_being_sent'),
});
};
};

@ -1,7 +1,7 @@
import { Table } from '@rocket.chat/fuselage';
import React, { useMemo, useCallback } from 'react';
import { GenericTable, Th } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
@ -21,9 +21,9 @@ export function OAuthAppsTable() {
}), [router]);
const header = useMemo(() => [
<Th key={'name'}>{t('Name')}</Th>,
<Th key={'_createdBy'}>{t('Created_by')}</Th>,
<Th key={'_createdAt'}>{t('Created_at')}</Th>,
<GenericTable.HeaderCell key={'name'}>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'_createdBy'}>{t('Created_by')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'_createdAt'}>{t('Created_at')}</GenericTable.HeaderCell>,
], [t]);
const renderRow = useCallback(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) =>

@ -12,7 +12,6 @@ import {
TextAreaInput,
ToggleSwitch,
FieldGroup,
Modal,
} from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
@ -22,45 +21,8 @@ import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import VerticalBar from '../../components/basic/VerticalBar';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Application_delete_warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Your_entry_has_been_deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
import DeleteSuccessModal from '../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../components/DeleteWarningModal';
export default function EditOauthAppWithData({ _id, ...props }) {
const t = useTranslation();
@ -139,13 +101,20 @@ function EditOauthApp({ onChange, data, ...props }) {
const onDeleteConfirm = useCallback(async () => {
try {
await deleteApp(data._id);
setModal(() => <SuccessModal onClose={() => { setModal(); close(); }}/>);
setModal(() => <DeleteSuccessModal
children={t('Your_entry_has_been_deleted')}
onClose={() => { setModal(); close(); }}
/>);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [close, data._id, deleteApp, dispatchToastMessage]);
}, [close, data._id, deleteApp, dispatchToastMessage, setModal, t]);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal
children={t('Application_delete_warning')}
onDelete={onDeleteConfirm}
onCancel={() => setModal(undefined)}
/>);
const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) });

@ -5,7 +5,7 @@ import { css } from '@rocket.chat/css-in-js';
import Page from '../../components/basic/Page';
import PermissionsContextBar from './PermissionsContextBar';
import { GenericTable } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';

@ -5,7 +5,7 @@ import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hoo
import UserAvatar from '../../components/basic/avatar/UserAvatar';
import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useMethod } from '../../contexts/ServerContext';
import { GenericTable } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useSetModal } from '../../contexts/ModalContext';
@ -61,9 +61,11 @@ export function UsersInRoleTable({ data, reload, roleName, total, params, setPar
closeModal();
reload();
};
setModal(<DeleteWarningModal onCancel={closeModal} onDelete={remove}>
{t('The_user_s_will_be_removed_from_role_s', username, roleName)}
</DeleteWarningModal>);
setModal(<DeleteWarningModal
children={t('The_user_s_will_be_removed_from_role_s', username, roleName)}
onCancel={closeModal}
onDelete={remove}
/>);
});
return <GenericTable

@ -2,7 +2,7 @@ import { Box, Table, Icon, TextInput, Field, CheckBox, Margins } from '@rocket.c
import { useMediaQuery, useUniqueId, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { GenericTable, Th } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import RoomAvatar from '../../components/basic/avatar/RoomAvatar';
import { roomTypes } from '../../../app/utils/client';
@ -128,12 +128,12 @@ function RoomsTable() {
}
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>{t('Name')}</Th>,
<Th key={'type'} direction={sort[1]} active={sort[0] === 't'} onClick={onHeaderClick} sort='t' w='x100'>{t('Type')}</Th>,
<Th key={'users'} direction={sort[1]} active={sort[0] === 'usersCount'} onClick={onHeaderClick} sort='usersCount' w='x80'>{t('Users')}</Th>,
mediaQuery && <Th key={'messages'} direction={sort[1]} active={sort[0] === 'msgs'} onClick={onHeaderClick} sort='msgs' w='x80'>{t('Msgs')}</Th>,
mediaQuery && <Th key={'default'} direction={sort[1]} active={sort[0] === 'default'} onClick={onHeaderClick} sort='default' w='x80' >{t('Default')}</Th>,
mediaQuery && <Th key={'featured'} direction={sort[1]} active={sort[0] === 'featured'} onClick={onHeaderClick} sort='featured' w='x80'>{t('Featured')}</Th>,
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'type'} direction={sort[1]} active={sort[0] === 't'} onClick={onHeaderClick} sort='t' w='x100'>{t('Type')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'users'} direction={sort[1]} active={sort[0] === 'usersCount'} onClick={onHeaderClick} sort='usersCount' w='x80'>{t('Users')}</GenericTable.HeaderCell>,
mediaQuery && <GenericTable.HeaderCell key={'messages'} direction={sort[1]} active={sort[0] === 'msgs'} onClick={onHeaderClick} sort='msgs' w='x80'>{t('Msgs')}</GenericTable.HeaderCell>,
mediaQuery && <GenericTable.HeaderCell key={'default'} direction={sort[1]} active={sort[0] === 'default'} onClick={onHeaderClick} sort='default' w='x80' >{t('Default')}</GenericTable.HeaderCell>,
mediaQuery && <GenericTable.HeaderCell key={'featured'} direction={sort[1]} active={sort[0] === 'featured'} onClick={onHeaderClick} sort='featured' w='x80'>{t('Featured')}</GenericTable.HeaderCell>,
].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]);
const renderRow = useCallback(({ _id, name, t: type, usersCount, msgs, default: isDefault, featured, usernames, ...args }) => {
@ -162,7 +162,15 @@ function RoomsTable() {
</Table.Row>;
}, [mediaQuery, onClick, t]);
return <GenericTable FilterComponent={FilterByTypeAndText} header={header} renderRow={renderRow} results={data.rooms} total={data.total} setParams={setParams} params={params}/>;
return <GenericTable
header={header}
renderRow={renderRow}
results={data.rooms}
total={data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByTypeAndText setFilter={onChange} {...props} />}
/>;
}
export default RoomsTable;

@ -1,106 +1,16 @@
import { Box, Icon, SearchInput, Skeleton } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { useSubscription } from 'use-subscription';
import React, { useCallback, useMemo, useEffect, memo } from 'react';
import { menu, SideNav, Layout } from '../../../app/ui-utils/client';
import { SettingType } from '../../../definition/ISetting';
import { useSettings } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoutePath, useCurrentRoute } from '../../contexts/RouterContext';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import PlanTag from '../../components/basic/PlanTag';
import Sidebar from '../../components/basic/Sidebar';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import { useRoutePath, useCurrentRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import SettingsProvider from '../../providers/SettingsProvider';
import { itemsSubscription } from '../sidebarItems';
import PlanTag from '../../components/basic/PlanTag';
const AdminSidebarPages = React.memo(({ currentPath }) => {
const items = useSubscription(itemsSubscription);
return <Box display='flex' flexDirection='column' flexShrink={0} pb='x8'>
<Sidebar.ItemsAssembler items={items} currentPath={currentPath}/>
</Box>;
});
const useSettingsGroups = (filter) => {
const settings = useSettings();
const t = useTranslation();
import AdminSidebarPages from './AdminSidebarPages';
import AdminSidebarSettings from './AdminSidebarSettings';
const filterPredicate = useMemo(() => {
if (!filter) {
return () => true;
}
const getMatchableStrings = (setting) => [
setting.i18nLabel && t(setting.i18nLabel),
t(setting._id),
setting._id,
].filter(Boolean);
try {
const filterRegex = new RegExp(filter, 'i');
return (setting) =>
getMatchableStrings(setting).some((text) => filterRegex.test(text));
} catch (e) {
return (setting) =>
getMatchableStrings(setting).some((text) => text.slice(0, filter.length) === filter);
}
}, [filter, t]);
return useMemo(() => {
const groupIds = Array.from(new Set(
settings
.filter(filterPredicate)
.map((setting) => {
if (setting.type === SettingType.GROUP) {
return setting._id;
}
return setting.group;
}),
));
return settings
.filter(({ type, group, _id }) => type === SettingType.GROUP && groupIds.includes(group || _id))
.sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id)));
}, [settings, filterPredicate, t]);
};
const AdminSidebarSettings = ({ currentPath }) => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []);
const groups = useSettingsGroups(useDebouncedValue(filter, 400));
const isLoadingGroups = false; // TODO: get from PrivilegedSettingsContext
return <Box is='section' display='flex' flexDirection='column' flexShrink={0} pb='x24'>
<Box pi='x24' pb='x8' fontScale='p2' color='info'>{t('Settings')}</Box>
<Box pi='x24' pb='x8' display='flex'>
<SearchInput
value={filter}
placeholder={t('Search')}
onChange={handleChange}
addon={<Icon name='magnifier' size='x20'/>}
/>
</Box>
<Box pb='x16' display='flex' flexDirection='column'>
{isLoadingGroups && <Skeleton/>}
{!isLoadingGroups && !!groups.length && <Sidebar.ItemsAssembler
items={groups.map((group) => ({
name: t(group.i18nLabel || group._id),
pathSection: 'admin',
pathGroup: group._id,
}))}
currentPath={currentPath}
/>}
{!isLoadingGroups && !groups.length && <Box pi='x28' mb='x4' color='hint'>{t('Nothing_found')}</Box>}
</Box>
</Box>;
};
export default React.memo(function AdminSidebar() {
function AdminSidebar() {
const t = useTranslation();
const canViewSettings = useAtLeastOnePermission(
@ -140,4 +50,6 @@ export default React.memo(function AdminSidebar() {
</Sidebar.Content>
</Sidebar>
</SettingsProvider>;
});
}
export default memo(AdminSidebar);

@ -0,0 +1,20 @@
import { Box } from '@rocket.chat/fuselage';
import React, { memo, FC } from 'react';
import { useSubscription } from 'use-subscription';
import Sidebar from '../../components/basic/Sidebar';
import { itemsSubscription } from '../sidebarItems';
type AdminSidebarPagesProps = {
currentPath: string;
};
const AdminSidebarPages: FC<AdminSidebarPagesProps> = ({ currentPath }) => {
const items = useSubscription(itemsSubscription);
return <Box display='flex' flexDirection='column' flexShrink={0} pb='x8'>
<Sidebar.ItemsAssembler items={items} currentPath={currentPath}/>
</Box>;
};
export default memo(AdminSidebarPages);

@ -0,0 +1,92 @@
import { Box, Icon, SearchInput, Skeleton } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useMemo, FC } from 'react';
import { ISetting, SettingType } from '../../../definition/ISetting';
import { useSettings } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import Sidebar from '../../components/basic/Sidebar';
const useSettingsGroups = (filter: string): ISetting[] => {
const settings = useSettings();
const t = useTranslation();
const filterPredicate = useMemo(() => {
if (!filter) {
return (): boolean => true;
}
const getMatchableStrings = (setting: ISetting): string[] => [
setting.i18nLabel && t(setting.i18nLabel),
t(setting._id),
setting._id,
].filter(Boolean);
try {
const filterRegex = new RegExp(filter, 'i');
return (setting: ISetting): boolean =>
getMatchableStrings(setting).some((text) => filterRegex.test(text));
} catch (e) {
return (setting: ISetting): boolean =>
getMatchableStrings(setting).some((text) => text.slice(0, filter.length) === filter);
}
}, [filter, t]);
return useMemo(() => {
const groupIds = Array.from(new Set(
settings
.filter(filterPredicate)
.map((setting) => {
if (setting.type === SettingType.GROUP) {
return setting._id;
}
return setting.group;
}),
));
return settings
.filter(({ type, group, _id }) => type === SettingType.GROUP && groupIds.includes(group || _id))
.sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id)));
}, [settings, filterPredicate, t]);
};
type AdminSidebarSettingsProps = {
currentPath: string;
};
const AdminSidebarSettings: FC<AdminSidebarSettingsProps> = ({ currentPath }) => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []);
const groups = useSettingsGroups(useDebouncedValue(filter, 400));
const isLoadingGroups = false; // TODO: get from PrivilegedSettingsContext
return <Box is='section' display='flex' flexDirection='column' flexShrink={0} pb='x24'>
<Box pi='x24' pb='x8' fontScale='p2' color='info'>{t('Settings')}</Box>
<Box pi='x24' pb='x8' display='flex'>
<SearchInput
value={filter}
placeholder={t('Search')}
onChange={handleChange}
addon={<Icon name='magnifier' size='x20'/>}
/>
</Box>
<Box pb='x16' display='flex' flexDirection='column'>
{isLoadingGroups && <Skeleton/>}
{!isLoadingGroups && !!groups.length && <Sidebar.ItemsAssembler
items={groups.map((group) => ({
name: t(group.i18nLabel || group._id),
pathSection: 'admin',
pathGroup: group._id,
}))}
currentPath={currentPath}
/>}
{!isLoadingGroups && !groups.length && <Box pi='x28' mb='x4' color='hint'>{t('Nothing_found')}</Box>}
</Box>
</Box>;
};
export default AdminSidebarSettings;

@ -1,4 +1,4 @@
import { Button, ButtonGroup, Icon, Menu, Modal, Option } from '@rocket.chat/fuselage';
import { ButtonGroup, Menu, Option } from '@rocket.chat/fuselage';
import React, { useCallback, useMemo } from 'react';
import { useUserInfoActionsSpread } from '../../channel/hooks/useUserInfoActions';
@ -11,46 +11,8 @@ import { useMethod, useEndpoint } from '../../contexts/ServerContext';
import { useSetting } from '../../contexts/SettingsContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useTranslation } from '../../contexts/TranslationContext';
const ConfirmWarningModal = ({ onConfirm, onCancel, confirmText, text, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{text}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onConfirm}>{confirmText}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, title, text, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{text}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
import DeleteSuccessModal from '../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../components/DeleteWarningModal';
export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) => {
const t = useTranslation();
@ -103,7 +65,10 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
const result = await deleteUserEndpoint(deleteUserQuery);
if (result.success) {
setModal(<SuccessModal title={t('Deleted')} text={t('User_has_been_deleted')} onClose={() => { setModal(); onChange(); }}/>);
setModal(<DeleteSuccessModal
children={t('User_has_been_deleted')}
onClose={() => { setModal(); onChange(); }}
/>);
} else {
setModal();
}
@ -113,7 +78,11 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
});
const confirmDeleteUser = useCallback(() => {
setModal(<ConfirmWarningModal onConfirm={deleteUser} onCancel={() => setModal()} text={t(`Delete_User_Warning_${ erasureType }`)} confirmText={t('Delete')} />);
setModal(<DeleteWarningModal
children={t(`Delete_User_Warning_${ erasureType }`)}
onCancel={() => setModal()}
onDelete={deleteUser}
/>);
}, [deleteUser, erasureType, setModal, t]);
const setAdminStatus = useMethod('setAdminStatus');
@ -134,12 +103,20 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
const result = await resetE2EEKeyRequest({ userId: _id });
if (result) {
setModal(<SuccessModal title={t('Success')} text={t('Users_key_has_been_reset')} onClose={() => { setModal(); onChange(); }}/>);
setModal(<DeleteSuccessModal
children={t('Users_key_has_been_reset')}
onClose={() => { setModal(); onChange(); }}
/>);
}
}, [resetE2EEKeyRequest, onChange, setModal, t, _id]);
const confirmResetE2EEKey = useCallback(() => {
setModal(<ConfirmWarningModal onConfirm={resetE2EEKey} onCancel={() => setModal()} text={t('E2E_Reset_Other_Key_Warning')} confirmText={t('Reset')} />);
setModal(<DeleteWarningModal
children={t('E2E_Reset_Other_Key_Warning')}
deleteText={t('Reset')}
onCancel={() => setModal()}
onDelete={resetE2EEKey}
/>);
}, [resetE2EEKey, t, setModal]);
const activeStatusQuery = useMemo(() => ({

@ -1,27 +1,19 @@
import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage';
import { Box, Table } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import React, { useMemo, useCallback, useState } from 'react';
import UserAvatar from '../../components/basic/avatar/UserAvatar';
import { GenericTable } from '../../components/GenericTable';
import GenericTable from '../../components/GenericTable';
import { capitalize } from '../../helpers/capitalize';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointData } from '../../hooks/useEndpointData';
import FilterByText from '../../components/FilterByText';
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' };
const FilterByText = ({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput flexShrink={0} placeholder={t('Search_Users')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
const style = {
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
};
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
@ -98,7 +90,6 @@ export function UsersTable() {
const mediaQuery = useMediaQuery('(min-width: 1024px)');
return <GenericTable
FilterComponent={FilterByText}
header={<>
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w='x200'>
{t('Name')}
@ -120,6 +111,7 @@ export function UsersTable() {
total={data.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }) => <FilterByText placeholder={t('Search_Users')} onChange={onChange} {...props} />}
>
{(props) => <UserRow key={props._id} onClick={onClick} mediaQuery={mediaQuery} {...props}/>}
</GenericTable>;

@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo } from 'react';
import { Field, TextInput, Select, ButtonGroup, Button, Box, Icon, Callout, FieldGroup } from '@rocket.chat/fuselage';
import { css } from '@rocket.chat/css-in-js';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import toastr from 'toastr';
import VerticalBar from '../../components/basic/VerticalBar';
import { UserAutoComplete } from '../../components/basic/AutoComplete';
@ -10,7 +9,8 @@ import { useTranslation } from '../../contexts/TranslationContext';
import { useForm } from '../../hooks/useForm';
import { useUserRoom } from '../hooks/useUserRoom';
import { useEndpoint } from '../../contexts/ServerContext';
import { roomTypes, isEmail } from '../../../app/utils';
import { roomTypes, isEmail } from '../../../app/utils/client';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
const clickable = css`
cursor: pointer;
@ -44,6 +44,8 @@ const FileExport = ({ onCancel, rid }) => {
const roomsExport = useEndpoint('POST', 'rooms.export');
const dispatchToastMessage = useToastMessageDispatch();
const handleSubmit = async () => {
try {
await roomsExport({
@ -54,10 +56,15 @@ const FileExport = ({ onCancel, rid }) => {
format,
});
toastr.success(t('Your_email_has_been_queued_for_sending'));
return;
dispatchToastMessage({
type: 'success',
message: t('Your_email_has_been_queued_for_sending'),
});
} catch (error) {
toastr.error(t('Error'));
dispatchToastMessage({
type: 'error',
message: error,
});
}
};
@ -111,14 +118,14 @@ const MailExportForm = ({ onCancel, rid }) => {
subject: t('Mail_Messages_Subject', roomName),
});
const dispatchToastMessage = useToastMessageDispatch();
const {
toUsers,
additionalEmails,
subject,
} = values;
const add = useMutableCallback((id) => setSelected(selectedMessages.concat(id)));
const remove = useMutableCallback((id) => setSelected(selectedMessages.filter((message) => message !== id)));
const reset = useMutableCallback(() => {
setSelected([]);
$(`#chat-window-${ rid }.messages-box .message.selected`)
@ -128,15 +135,16 @@ const MailExportForm = ({ onCancel, rid }) => {
useEffect(() => {
const $root = $(`#chat-window-${ rid }`);
$('.messages-box', $root).addClass('selectable');
const handler = function() {
const { id } = this;
if (this.classList.contains('selected')) {
this.classList.remove('selected');
remove(id);
setSelected((selectedMessages) => selectedMessages.filter((message) => message !== id));
} else {
this.classList.add('selected');
add(id);
setSelected((selectedMessages) => selectedMessages.concat(id));
}
};
$('.messages-box .message', $root).on('click', handler);
@ -183,10 +191,15 @@ const MailExportForm = ({ onCancel, rid }) => {
messages: selectedMessages,
});
toastr.success(t('Your_email_has_been_queued_for_sending'));
return;
dispatchToastMessage({
type: 'success',
message: t('Your_email_has_been_queued_for_sending'),
});
} catch (error) {
toastr.error(t('Error'));
dispatchToastMessage({
type: 'error',
message: error,
});
}
};
@ -218,7 +231,7 @@ const MailExportForm = ({ onCancel, rid }) => {
</Field.Row>
</Field>
{ errorMessage && <Callout type={'danger'} title={errorMessage} /> }
{errorMessage && <Callout type={'danger'} title={errorMessage} />}
<ButtonGroup stretch mb='x12'>
<Button onClick={onCancel}>

@ -0,0 +1,33 @@
import React, { FC } from 'react';
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '../contexts/TranslationContext';
type DeleteSuccessModalProps = {
onClose: () => void;
};
const DeleteSuccessModal: FC<DeleteSuccessModalProps> = ({
children,
onClose,
...props
}) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{children}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default DeleteSuccessModal;

@ -1,26 +0,0 @@
import { Icon, Button, Modal, ButtonGroup } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../contexts/TranslationContext';
const DeleteWarningModal = ({ onDelete, onCancel, children, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content>
{children}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default DeleteWarningModal;

@ -0,0 +1,41 @@
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { useTranslation } from '../contexts/TranslationContext';
type DeleteWarningModalProps = {
cancelText?: string;
deleteText?: string;
onDelete: () => void;
onCancel: () => void;
};
const DeleteWarningModal: FC<DeleteWarningModalProps> = ({
children,
cancelText,
deleteText,
onCancel,
onDelete,
...props
}) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{children}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{cancelText ?? t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{deleteText ?? t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default DeleteWarningModal;

@ -0,0 +1,37 @@
import { Box, Icon, TextInput } from '@rocket.chat/fuselage';
import React, { FC, ChangeEvent, FormEvent, memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from '../contexts/TranslationContext';
type FilterByTextProps = {
placeholder?: string;
onChange: (filter: { text: string }) => void;
};
const FilterByText: FC<FilterByTextProps> = ({
placeholder,
onChange: setFilter,
...props
}) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
}, []);
useEffect(() => {
setFilter({ text });
}, [setFilter, text]);
const handleFormSubmit = useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
}, []);
return <Box mb='x16' is='form' onSubmit={handleFormSubmit} display='flex' flexDirection='column' {...props}>
<TextInput placeholder={placeholder ?? t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleInputChange} value={text} />
</Box>;
};
export default memo(FilterByText);

@ -1,7 +1,7 @@
import React from 'react';
import { TextInput, Box, Icon } from '@rocket.chat/fuselage';
import { GenericTable, Th } from './GenericTable';
import GenericTable from './GenericTable';
export default {
@ -18,10 +18,13 @@ export const _default = () => {
const header = [
<Th>Name</Th>,
<Th>Email</Th>,
<Th>Data</Th>,
<Th>Info</Th>,
<GenericTable.HeaderCell>Name</GenericTable.HeaderCell>,
<GenericTable.HeaderCell>Email</GenericTable.HeaderCell>,
<GenericTable.HeaderCell>Data</GenericTable.HeaderCell>,
<GenericTable.HeaderCell>Info</GenericTable.HeaderCell>,
];
return <GenericTable FilterComponent={Search} header={header} />;
return <GenericTable
header={header}
renderFilter={(props) => <Search {...props} />}
/>;
};

@ -0,0 +1,30 @@
import { Box, Table } from '@rocket.chat/fuselage';
import React, { FC, useCallback } from 'react';
import SortIcon from './SortIcon';
type HeaderCellProps = {
active?: boolean;
direction?: 'asc' | 'desc';
sort?: boolean;
onClick?: (sort: boolean) => void;
};
const HeaderCell: FC<HeaderCellProps> = ({
children,
active,
direction,
sort,
onClick,
...props
}) => {
const fn = useCallback(() => onClick && onClick(!!sort), [sort, onClick]);
return <Table.Cell clickable={!!sort} onClick={fn} { ...props }>
<Box display='flex' alignItems='center' wrap='no-wrap'>
{children}
{sort && <SortIcon direction={active ? direction : undefined} />}
</Box>
</Table.Cell>;
};
export default HeaderCell;

@ -0,0 +1,24 @@
import { Box, Skeleton, Table } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
type LoadingRowProps = {
cols: number;
};
const LoadingRow: FC<LoadingRowProps> = ({ cols }) =>
<Table.Row>
<Table.Cell>
<Box display='flex'>
<Skeleton variant='rect' height={40} width={40} />
<Box mi='x8' flexGrow={1}>
<Skeleton width='100%' />
<Skeleton width='100%' />
</Box>
</Box>
</Table.Cell>
{Array.from({ length: cols - 1 }, (_, i) => <Table.Cell key={i}>
<Skeleton width='100%' />
</Table.Cell>)}
</Table.Row>;
export default LoadingRow;

@ -0,0 +1,14 @@
import { Box } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
type SortIconProps = {
direction?: 'asc' | 'desc';
};
const SortIcon: FC<SortIconProps> = ({ direction }) =>
<Box is='svg' width='x16' height='x16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M5.33337 5.99999L8.00004 3.33333L10.6667 5.99999' stroke={direction === 'desc' ? '#9EA2A8' : '#E4E7EA' } strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/>
<path d='M5.33337 10L8.00004 12.6667L10.6667 10' stroke={direction === 'asc' ? '#9EA2A8' : '#E4E7EA'} strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/>
</Box>;
export default SortIcon;

@ -1,53 +1,24 @@
import { Box, Pagination, Skeleton, Table, Flex, Tile, Scrollable } from '@rocket.chat/fuselage';
import { Box, Pagination, Table, Tile, Scrollable } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useState, useEffect, useCallback, forwardRef } from 'react';
import React, { useState, useEffect, useCallback, forwardRef } from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import { useTranslation } from '../contexts/TranslationContext';
import { useTranslation } from '../../contexts/TranslationContext';
import HeaderCell from './HeaderCell';
import LoadingRow from './LoadingRow';
function SortIcon({ direction }) {
return <Box is='svg' width='x16' height='x16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M5.33337 5.99999L8.00004 3.33333L10.6667 5.99999' stroke={direction === 'desc' ? '#9EA2A8' : '#E4E7EA' } strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/>
<path d='M5.33337 10L8.00004 12.6667L10.6667 10' stroke={ direction === 'asc' ? '#9EA2A8' : '#E4E7EA'} strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/>
</Box>;
}
export function Th({ children, active, direction, sort, onClick, align, ...props }) {
const fn = useMemo(() => () => onClick && onClick(sort), [sort, onClick]);
return <Table.Cell clickable={!!sort} onClick={fn} { ...props }>
<Box display='flex' alignItems='center' wrap='no-wrap'>{children}{sort && <SortIcon direction={active && direction} />}</Box>
</Table.Cell>;
}
const LoadingRow = ({ cols }) => <Table.Row>
<Table.Cell>
<Box display='flex'>
<Flex.Item>
<Skeleton variant='rect' height={40} width={40} />
</Flex.Item>
<Box mi='x8' flexGrow={1}>
<Skeleton width='100%' />
<Skeleton width='100%' />
</Box>
</Box>
</Table.Cell>
{ Array.from({ length: cols - 1 }, (_, i) => <Table.Cell key={i}>
<Skeleton width='100%' />
</Table.Cell>)}
</Table.Row>;
export const GenericTable = forwardRef(function GenericTable({
const GenericTable = ({
children,
results,
fixed = true,
total,
renderRow: RenderRow,
header,
setParams = () => { },
params: paramsDefault = '',
FilterComponent = () => null,
renderFilter,
renderRow: RenderRow,
results,
setParams = () => { },
total,
...props
}, ref) {
}, ref) => {
const t = useTranslation();
const [filter, setFilter] = useState(paramsDefault);
@ -72,7 +43,7 @@ export const GenericTable = forwardRef(function GenericTable({
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]);
return <>
<FilterComponent setFilter={setFilter} { ...props}/>
{typeof renderFilter === 'function' ? renderFilter({ onChange: setFilter, ...props }) : null}
{results && !results.length
? <Tile fontScale='p1' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}
@ -110,8 +81,8 @@ export const GenericTable = forwardRef(function GenericTable({
</>
}
</>;
});
};
export default Object.assign(GenericTable, {
HeaderCell: Th,
export default Object.assign(forwardRef(GenericTable), {
HeaderCell,
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save