From fe0c27ab3812800bf389a3bb8564a90a56f5045e Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 13 Oct 2020 08:08:02 -0700 Subject: [PATCH] Refactor some React Pages and Components (#19202) --- client/account/AccountProfileForm.js | 4 +- client/account/AccountProfilePage.js | 30 +--- client/account/AccountSidebar.js | 8 +- client/account/ActionConfirmModal.tsx | 48 +++++ client/account/preferences/MyDataModal.tsx | 32 ++++ .../preferences/PreferencesMyDataSection.js | 23 +-- client/account/security/BackupCodesModal.tsx | 36 ++++ client/account/security/TwoFactorTOTP.js | 73 +------- client/account/security/VerifyCodeModal.tsx | 55 ++++++ client/account/tokens/AccountTokensRow.tsx | 45 +++++ client/account/tokens/AccountTokensTable.js | 51 ++---- .../tokens/{InfoModal.js => InfoModal.tsx} | 23 ++- client/admin/apps/APIsDisplay.tsx | 40 +++++ client/admin/apps/AppDetailsPage.js | 166 ++---------------- client/admin/apps/AppDetailsPageContent.tsx | 106 +++++++++++ client/admin/apps/AppLogsPage.js | 41 +---- client/admin/apps/AppMenu.js | 4 +- client/admin/apps/AppProvider.tsx | 20 +-- client/admin/apps/AppRow.tsx | 94 ++++++++++ client/admin/apps/AppStatus.js | 10 +- client/admin/apps/AppsTable.js | 106 +---------- client/admin/apps/CloudLoginModal.js | 4 +- client/admin/apps/IframeModal.js | 4 +- client/admin/apps/LoadingDetails.tsx | 14 ++ client/admin/apps/LogEntry.tsx | 31 ++++ client/admin/apps/LogItem.tsx | 33 ++++ client/admin/apps/LogsLoading.tsx | 13 ++ client/admin/apps/MarketplaceRow.tsx | 103 +++++++++++ client/admin/apps/MarketplaceTable.js | 114 +----------- client/admin/apps/PriceDisplay.js | 4 +- client/admin/apps/SettingsDisplay.tsx | 50 ++++++ client/admin/apps/types.ts | 27 +++ client/admin/cloud/CopyStep.tsx | 87 +++++++++ .../cloud/ManualWorkspaceRegistrationModal.js | 150 +--------------- client/admin/cloud/PasteStep.tsx | 84 +++++++++ client/admin/customEmoji/AddCustomEmoji.js | 4 +- client/admin/customEmoji/CustomEmoji.js | 38 ++-- .../admin/customEmoji/CustomEmoji.stories.js | 0 client/admin/customEmoji/CustomEmojiRoute.js | 11 +- ...EditCustomEmoji.js => EditCustomEmoji.tsx} | 111 ++++-------- .../customEmoji/EditCustomEmojiWithData.tsx | 60 +++++++ client/admin/customEmoji/types.ts | 6 + client/admin/customSounds/AddCustomSound.js | 4 +- client/admin/customSounds/AdminSounds.js | 40 ++--- client/admin/customSounds/AdminSoundsRoute.js | 6 +- client/admin/customSounds/EditCustomSound.js | 58 ++---- .../customUserStatus/AddCustomUserStatus.js | 4 +- .../customUserStatus/CustomUserStatus.js | 38 ++-- .../customUserStatus/CustomUserStatusRoute.js | 10 +- .../customUserStatus/EditCustomUserStatus.js | 89 ++-------- .../EditCustomUserStatusWithData.tsx | 49 ++++++ .../FederationDashboardRoute.tsx | 2 +- .../federationDashboard/OverviewSection.js | 4 +- .../federationDashboard/ServersSection.js | 4 +- client/admin/import/PrepareChannels.tsx | 95 ++++++++++ client/admin/import/PrepareImportPage.js | 140 +-------------- client/admin/import/PrepareUsers.tsx | 94 ++++++++++ client/admin/info/BuildEnvironmentSection.js | 6 +- .../info/BuildEnvironmentSection.stories.js | 2 +- client/admin/info/CommitSection.js | 6 +- client/admin/info/CommitSection.stories.js | 2 +- client/admin/info/DescriptionList.js | 8 +- client/admin/info/DescriptionList.stories.js | 2 +- client/admin/info/InformationPage.js | 16 +- client/admin/info/InformationPage.stories.js | 2 +- client/admin/info/InformationRoute.js | 5 +- client/admin/info/InstancesSection.js | 6 +- client/admin/info/InstancesSection.stories.js | 2 +- client/admin/info/RocketChatSection.js | 6 +- .../admin/info/RocketChatSection.stories.js | 2 +- .../admin/info/RuntimeEnvironmentSection.js | 6 +- .../info/RuntimeEnvironmentSection.stories.js | 2 +- client/admin/info/UsageSection.js | 6 +- client/admin/info/UsageSection.stories.js | 2 +- .../admin/integrations/IntegrationsTable.js | 23 ++- .../integrations/edit/EditIncomingWebhook.js | 18 +- .../integrations/edit/EditIntegrationsPage.js | 41 +---- .../integrations/edit/EditOutgoingWebhook.js | 18 +- client/admin/invites/InvitesPage.js | 2 +- client/admin/mailer/MailerRoute.js | 25 ++- client/admin/oauthApps/OAuthAppsTable.js | 8 +- client/admin/oauthApps/OAuthEditApp.js | 55 ++---- client/admin/permissions/PermissionsTable.js | 2 +- client/admin/permissions/UsersInRoleTable.js | 10 +- client/admin/rooms/RoomsTable.js | 24 ++- client/admin/sidebar/AdminSidebar.js | 110 ++---------- client/admin/sidebar/AdminSidebarPages.tsx | 20 +++ client/admin/sidebar/AdminSidebarSettings.tsx | 92 ++++++++++ client/admin/users/UserInfoActions.js | 67 +++---- client/admin/users/UsersTable.js | 26 +-- client/channel/ExportMessages/index.js | 39 ++-- client/components/DeleteSuccessModal.tsx | 33 ++++ client/components/DeleteWarningModal.js | 26 --- client/components/DeleteWarningModal.tsx | 41 +++++ client/components/FilterByText.tsx | 37 ++++ client/components/GenericTable.stories.js | 15 +- client/components/GenericTable/HeaderCell.tsx | 30 ++++ client/components/GenericTable/LoadingRow.tsx | 24 +++ client/components/GenericTable/SortIcon.tsx | 14 ++ .../index.js} | 61 ++----- client/contexts/ConnectionStatusContext.ts | 8 +- client/contexts/CustomSoundContext.ts | 2 +- client/contexts/TranslationContext.js | 20 --- client/contexts/TranslationContext.ts | 45 +++++ client/omnichannel/agents/AgentsPage.js | 35 ++-- client/omnichannel/agents/AgentsRoute.js | 22 ++- .../currentChats/CurrentChatsPage.js | 18 +- .../currentChats/CurrentChatsRoute.js | 21 ++- .../customFields/CustomFieldsTable.js | 5 +- .../departments/DepartmentsAgentsTable.js | 15 +- .../departments/DepartmentsPage.js | 34 ++-- .../departments/DepartmentsRoute.js | 14 +- client/omnichannel/managers/ManagersPage.js | 34 ++-- client/omnichannel/managers/ManagersRoute.js | 10 +- client/omnichannel/triggers/TriggersTable.js | 5 +- ...arUrlProvider.js => AvatarUrlProvider.tsx} | 8 +- client/providers/ConnectionStatusProvider.js | 16 -- client/providers/ConnectionStatusProvider.tsx | 18 ++ client/providers/CustomSoundProvider.tsx | 9 + client/providers/CustomSoundProvides.js | 9 - client/providers/MeteorProvider.js | 20 +-- client/providers/RouterProvider.tsx | 4 +- client/providers/ServerProvider.js | 7 +- client/providers/SidebarProvider.tsx | 4 +- client/providers/ToastMessagesProvider.tsx | 4 +- client/providers/TranslationProvider.js | 10 +- client/types/fuselage.d.ts | 108 +++++++++++- client/views/directory/ChannelsTab.js | 40 ++--- client/views/directory/UserTab.js | 50 +++--- ee/client/audit/AuditLogTable.js | 2 +- ee/client/omnichannel/BusinessHoursTable.js | 32 +--- ee/client/omnichannel/MonitorsTable.js | 25 +-- .../omnichannel/priorities/PrioritiesPage.js | 34 ++-- .../omnichannel/priorities/PrioritiesRoute.js | 10 +- ee/client/omnichannel/tags/TagsPage.js | 34 ++-- ee/client/omnichannel/tags/TagsRoute.js | 8 +- ee/client/omnichannel/units/UnitsPage.js | 34 ++-- ee/client/omnichannel/units/UnitsRoute.js | 8 +- package-lock.json | 53 +++--- package.json | 1 + 140 files changed, 2446 insertions(+), 1932 deletions(-) create mode 100644 client/account/ActionConfirmModal.tsx create mode 100644 client/account/preferences/MyDataModal.tsx create mode 100644 client/account/security/BackupCodesModal.tsx create mode 100644 client/account/security/VerifyCodeModal.tsx create mode 100644 client/account/tokens/AccountTokensRow.tsx rename client/account/tokens/{InfoModal.js => InfoModal.tsx} (61%) create mode 100644 client/admin/apps/APIsDisplay.tsx create mode 100644 client/admin/apps/AppDetailsPageContent.tsx create mode 100644 client/admin/apps/AppRow.tsx create mode 100644 client/admin/apps/LoadingDetails.tsx create mode 100644 client/admin/apps/LogEntry.tsx create mode 100644 client/admin/apps/LogItem.tsx create mode 100644 client/admin/apps/LogsLoading.tsx create mode 100644 client/admin/apps/MarketplaceRow.tsx create mode 100644 client/admin/apps/SettingsDisplay.tsx create mode 100644 client/admin/apps/types.ts create mode 100644 client/admin/cloud/CopyStep.tsx create mode 100644 client/admin/cloud/PasteStep.tsx delete mode 100644 client/admin/customEmoji/CustomEmoji.stories.js rename client/admin/customEmoji/{EditCustomEmoji.js => EditCustomEmoji.tsx} (53%) create mode 100644 client/admin/customEmoji/EditCustomEmojiWithData.tsx create mode 100644 client/admin/customEmoji/types.ts create mode 100644 client/admin/customUserStatus/EditCustomUserStatusWithData.tsx create mode 100644 client/admin/import/PrepareChannels.tsx create mode 100644 client/admin/import/PrepareUsers.tsx create mode 100644 client/admin/sidebar/AdminSidebarPages.tsx create mode 100644 client/admin/sidebar/AdminSidebarSettings.tsx create mode 100644 client/components/DeleteSuccessModal.tsx delete mode 100644 client/components/DeleteWarningModal.js create mode 100644 client/components/DeleteWarningModal.tsx create mode 100644 client/components/FilterByText.tsx create mode 100644 client/components/GenericTable/HeaderCell.tsx create mode 100644 client/components/GenericTable/LoadingRow.tsx create mode 100644 client/components/GenericTable/SortIcon.tsx rename client/components/{GenericTable.js => GenericTable/index.js} (53%) delete mode 100644 client/contexts/TranslationContext.js create mode 100644 client/contexts/TranslationContext.ts rename client/providers/{AvatarUrlProvider.js => AvatarUrlProvider.tsx} (67%) delete mode 100644 client/providers/ConnectionStatusProvider.js create mode 100644 client/providers/ConnectionStatusProvider.tsx create mode 100644 client/providers/CustomSoundProvider.tsx delete mode 100644 client/providers/CustomSoundProvides.js diff --git a/client/account/AccountProfileForm.js b/client/account/AccountProfileForm.js index 368889d16ba..aff7951e87d 100644 --- a/client/account/AccountProfileForm.js +++ b/client/account/AccountProfileForm.js @@ -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 ; } + +export default AccountProfileForm; diff --git a/client/account/AccountProfilePage.js b/client/account/AccountProfilePage.js index 86989d48051..722cd17380f 100644 --- a/client/account/AccountProfilePage.js +++ b/client/account/AccountProfilePage.js @@ -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 - - - {title} - - - - {text} - {isPassword && } - {!isPassword && } - - - - - - - - ; -}; +import ActionConfirmModal from './ActionConfirmModal'; const getInitialValues = (user) => ({ realname: user.name ?? '', diff --git a/client/account/AccountSidebar.js b/client/account/AccountSidebar.js index 3aa09ae3f94..b11fd7bd646 100644 --- a/client/account/AccountSidebar.js +++ b/client/account/AccountSidebar.js @@ -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() { ; -}); +}; + +export default memo(AccountSidebar); diff --git a/client/account/ActionConfirmModal.tsx b/client/account/ActionConfirmModal.tsx new file mode 100644 index 00000000000..db36c1765ef --- /dev/null +++ b/client/account/ActionConfirmModal.tsx @@ -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 = ({ + 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 + + + {title} + + + + {text} + {isPassword && } + {!isPassword && } + + + + + + + + ; +}; + +export default ActionConfirmModal; diff --git a/client/account/preferences/MyDataModal.tsx b/client/account/preferences/MyDataModal.tsx new file mode 100644 index 00000000000..2eb0fce5a5b --- /dev/null +++ b/client/account/preferences/MyDataModal.tsx @@ -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 = ({ onCancel, title, text, ...props }) => { + const t = useTranslation(); + + return + + + {title} + + + + {text} + + + + + + + ; +}; + +export default MyDataModal; diff --git a/client/account/preferences/PreferencesMyDataSection.js b/client/account/preferences/PreferencesMyDataSection.js index 46a5b223384..0630ac445e7 100644 --- a/client/account/preferences/PreferencesMyDataSection.js +++ b/client/account/preferences/PreferencesMyDataSection.js @@ -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 - - - {title} - - - - {text} - - - - - - - ; -}; +import MyDataModal from './MyDataModal'; const PreferencesMyDataSection = ({ onChange, ...props }) => { const t = useTranslation(); diff --git a/client/account/security/BackupCodesModal.tsx b/client/account/security/BackupCodesModal.tsx new file mode 100644 index 00000000000..70d782ee250 --- /dev/null +++ b/client/account/security/BackupCodesModal.tsx @@ -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 = ({ codes, onClose, ...props }) => { + const t = useTranslation(); + + const codesText = useMemo(() => codes.join(' '), [codes]); + + return + + + {t('Backup_codes')} + + + + {t('Make_sure_you_have_a_copy_of_your_codes_1')} + + {t('Make_sure_you_have_a_copy_of_your_codes_2')} + + + + + + + ; +}; + +export default BackupCodesModal; diff --git a/client/account/security/TwoFactorTOTP.js b/client/account/security/TwoFactorTOTP.js index ff809165044..5d81d6d8ed9 100644 --- a/client/account/security/TwoFactorTOTP.js +++ b/client/account/security/TwoFactorTOTP.js @@ -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 - - - {t('Backup_codes')} - - - - {t('Make_sure_you_have_a_copy_of_your_codes_1')} - - {t('Make_sure_you_have_a_copy_of_your_codes_2')} - - - - - - - ; -}; - -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 - - - {t('Two-factor_authentication')} - - - - {t('Open_your_authentication_app_and_enter_the_code')} - - - - - - - - - - - ; -}; +import BackupCodesModal from './BackupCodesModal'; +import VerifyCodeModal from './VerifyCodeModal'; const TwoFactorTOTP = (props) => { const t = useTranslation(); diff --git a/client/account/security/VerifyCodeModal.tsx b/client/account/security/VerifyCodeModal.tsx new file mode 100644 index 00000000000..08560b98548 --- /dev/null +++ b/client/account/security/VerifyCodeModal.tsx @@ -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 = ({ 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 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 + + + {t('Two-factor_authentication')} + + + + {t('Open_your_authentication_app_and_enter_the_code')} + + + + + + + + + + + ; +}; + +export default VerifyCodeModal; diff --git a/client/account/tokens/AccountTokensRow.tsx b/client/account/tokens/AccountTokensRow.tsx new file mode 100644 index 00000000000..5a05a7d9a09 --- /dev/null +++ b/client/account/tokens/AccountTokensRow.tsx @@ -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 = ({ + 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 + {name} + {isMedium && {formatDateAndTime(createdAt)}} + ...{lastTokenPart} + {bypassTwoFactor ? t('Ignore') : t('Require')} + + + + + + + ; +}; + +export default AccountTokensRow; diff --git a/client/account/tokens/AccountTokensTable.js b/client/account/tokens/AccountTokensTable.js index 75bd1eba4cd..e5bd1013038 100644 --- a/client/account/tokens/AccountTokensTable.js +++ b/client/account/tokens/AccountTokensTable.js @@ -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 - {name} - {isMedium && {formatDateAndTime(createdAt)}} - ...{lastTokenPart} - {bypassTwoFactor ? t('Ignore') : t('Require')} - - - - - - - ; -}; - -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(() => [ - {t('API_Personal_Access_Token_Name')}, - isMedium && {t('Created_at')}, - {t('Last_token_part')}, - {t('Two Factor Authentication')}, - , + {t('API_Personal_Access_Token_Name')}, + isMedium && {t('Created_at')}, + {t('Last_token_part')}, + {t('Two Factor Authentication')}, + , ].filter(Boolean), [isMedium, t]); const onRegenerate = useCallback((name) => { @@ -117,15 +98,13 @@ export function AccountTokensTable({ data, reload }) { }, [closeModal, dispatchToastMessage, reload, removeToken, setModal, t]); return - {useCallback((props) => , [formatDateAndTime, isMedium, onRegenerate, onRemove, t])} + />, [isMedium, onRegenerate, onRemove])} ; -} +}; export default AccountTokensTable; diff --git a/client/account/tokens/InfoModal.js b/client/account/tokens/InfoModal.tsx similarity index 61% rename from client/account/tokens/InfoModal.js rename to client/account/tokens/InfoModal.tsx index 1ace92d5372..84bab8e755f 100644 --- a/client/account/tokens/InfoModal.js +++ b/client/account/tokens/InfoModal.tsx @@ -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 = ({ + title, + content, + icon, + confirmText, + cancelText, + onConfirm, + onClose, + ...props +}) => {icon} diff --git a/client/admin/apps/APIsDisplay.tsx b/client/admin/apps/APIsDisplay.tsx new file mode 100644 index 00000000000..d95dff2a797 --- /dev/null +++ b/client/admin/apps/APIsDisplay.tsx @@ -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; + }[]; +}; + +const APIsDisplay: FC = ({ apis }) => { + const t = useTranslation(); + + const absoluteUrl = useAbsoluteUrl(); + + const getApiCurl = apiCurlGetter(absoluteUrl); + + return <> + + + {t('APIs')} + {apis.map((api) => + {api.methods.join(' | ').toUpperCase()} {api.path} + {api.methods.map((method) => +

+						{getApiCurl(method, api).map((curlAddress) => <>{curlAddress}
)} +
+
)} +
)} +
+ ; +}; + +export default APIsDisplay; diff --git a/client/admin/apps/AppDetailsPage.js b/client/admin/apps/AppDetailsPage.js index 66db8657f64..5a701260430 100644 --- a/client/admin/apps/AppDetailsPage.js +++ b/client/admin/apps/AppDetailsPage.js @@ -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 <> - - - - {name} - - {t('By_author', { author: authorName })} - | - {t('Version_version', { version })} - - - - - {!installed && } - - {installed && } - - - - - - - - {t('Categories')} - - {categories && categories.map((current) => {current})} - - - {t('Contact')} - - - {t('Author_Site')} - - - - {t('Support')} - - - - - {t('Details')} - {description} - - - - {bundledIn && <> - - - - {t('Bundles')} - {bundledIn.map((bundle) => - - {bundle.apps.map((app) => )} - - - {bundle.bundleName} - {bundle.apps.map((app) => {app.latest.name},)} - - )} - - - } - ; -} - -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 <> - - - {t('Settings')} - - - ; -}; - -const APIsDisplay = ({ apis }) => { - const t = useTranslation(); - - const absoluteUrl = useAbsoluteUrl(); - - const getApiCurl = apiCurlGetter(absoluteUrl); - - return <> - - - {t('APIs')} - {apis.map((api) => - {api.methods.join(' | ').toUpperCase()} {api.path} - {api.methods.map((method) => -

-						{getApiCurl(method, api).map((curlAddress) => <>{curlAddress}
)} -
-
)} -
)} -
- ; -}; - -const LoadingDetails = () => - - - - - - -; - -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 }) { ; } + +export default AppDetailsPage; diff --git a/client/admin/apps/AppDetailsPageContent.tsx b/client/admin/apps/AppDetailsPageContent.tsx new file mode 100644 index 00000000000..23d8a8d5560 --- /dev/null +++ b/client/admin/apps/AppDetailsPageContent.tsx @@ -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 = ({ data }) => { + const t = useTranslation(); + + const { + iconFileData = '', + name, + author: { name: authorName, homepage, support }, + description, + categories = [], + version, + price, + purchaseType, + pricingPlans, + iconFileContent, + installed, + bundledIn, + } = data; + + return <> + + + + {name} + + {t('By_author', { author: authorName })} + | + {t('Version_version', { version })} + + + + + {!installed && } + + {installed && } + + + + + + + {t('Categories')} + + {categories && categories.map((current) => + + {current} + )} + + + {t('Contact')} + + + {t('Author_Site')} + + + + {t('Support')} + + + + + {t('Details')} + {description} + + + {bundledIn && <> + + + + {t('Bundles')} + {bundledIn.map((bundle) => + + {bundle.apps.map((app) => )} + + + {bundle.bundleName} + {bundle.apps.map((app) => {app.latest.name},)} + + )} + + + } + ; +}; + +export default AppDetailsPageContent; diff --git a/client/admin/apps/AppLogsPage.js b/client/admin/apps/AppLogsPage.js index 4100300e5f6..e8361d61034 100644 --- a/client/admin/apps/AppLogsPage.js +++ b/client/admin/apps/AppLogsPage.js @@ -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 - {severity}: {timestamp} {t('Caller')}: {caller} - -
-				
-			
-
-
; -}; - -const LogItem = ({ entries, instanceId, title, t, ...props }) => - {instanceId && {t('Instance')}: {instanceId}} - {entries.map(({ severity, timestamp, caller, args }, i) => )} -; - -const LogsLoading = () => - - - - - -; +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} />)} } diff --git a/client/admin/apps/AppMenu.js b/client/admin/apps/AppMenu.js index a7cb1fbe3cc..538f678fc0f 100644 --- a/client/admin/apps/AppMenu.js +++ b/client/admin/apps/AppMenu.js @@ -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 }) { diff --git a/client/admin/apps/AppProvider.tsx b/client/admin/apps/AppProvider.tsx index 2cb3ab67eaf..8bb63c44730 100644 --- a/client/admin/apps/AppProvider.tsx +++ b/client/admin/apps/AppProvider.tsx @@ -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) => { 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, }; diff --git a/client/admin/apps/AppRow.tsx b/client/admin/apps/AppRow.tsx new file mode 100644 index 00000000000..db3734934d7 --- /dev/null +++ b/client/admin/apps/AppRow.tsx @@ -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 = ({ + 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): void => { + if (!['Enter', 'Space'].includes(e.nativeEvent.code)) { + return; + } + + handleClick(); + }; + + const preventClickPropagation = (e: MouseEvent): void => { + e.stopPropagation(); + }; + + return setFocused(true)} + onBlur={(): void => setFocused(false)} + onMouseEnter={(): void => setHovered(true)} + onMouseLeave={(): void => setHovered(false)} + > + + + + {name} + {`${ t('By') } ${ authorName }`} + + + {medium && + + {description} + {categories && + {categories.map((current) => {current})} + } + + } + + + + {installed && } + + + ; +}; + +export default memo(AppRow); diff --git a/client/admin/apps/AppStatus.js b/client/admin/apps/AppStatus.js index 0ae05ad4e4e..de92aaa75af 100644 --- a/client/admin/apps/AppStatus.js +++ b/client/admin/apps/AppStatus.js @@ -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)} } ; -}); +}; -export default AppStatus; +export default memo(AppStatus); diff --git a/client/admin/apps/AppsTable.js b/client/admin/apps/AppsTable.js index 22252e68e7e..3537f9246ac 100644 --- a/client/admin/apps/AppsTable.js +++ b/client/admin/apps/AppsTable.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}); - -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 setFocused(true)} - onBlur={() => setFocused(false)} - onMouseEnter={() => setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - - - - {name} - {`${ t('By') } ${ authorName }`} - - - {medium && - - {description} - {categories && - {categories.map((current) => {current})} - } - - } - - - - {installed && } - - - ; -}); +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 }) => } > {(props) => } ; diff --git a/client/admin/apps/CloudLoginModal.js b/client/admin/apps/CloudLoginModal.js index 192ee8124e3..6ad60d21c05 100644 --- a/client/admin/apps/CloudLoginModal.js +++ b/client/admin/apps/CloudLoginModal.js @@ -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) => {
; }; + +export default CloudLoginModal; diff --git a/client/admin/apps/IframeModal.js b/client/admin/apps/IframeModal.js index 9bd3e3afc6d..7e86e0db11e 100644 --- a/client/admin/apps/IframeModal.js +++ b/client/admin/apps/IframeModal.js @@ -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 }) => { ; }; + +export default IframeModal; diff --git a/client/admin/apps/LoadingDetails.tsx b/client/admin/apps/LoadingDetails.tsx new file mode 100644 index 00000000000..3d13437a5c5 --- /dev/null +++ b/client/admin/apps/LoadingDetails.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import { Box, Skeleton } from '@rocket.chat/fuselage'; + +const LoadingDetails: FC = () => + + + + + + + + ; + +export default LoadingDetails; diff --git a/client/admin/apps/LogEntry.tsx b/client/admin/apps/LogEntry.tsx new file mode 100644 index 00000000000..f62d93df77e --- /dev/null +++ b/client/admin/apps/LogEntry.tsx @@ -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 = ({ severity, timestamp, caller, args }) => { + const t = useTranslation(); + + return + {severity}: {timestamp} {t('Caller')}: {caller} + +
+				
+			
+
+
; +}; + +export default LogEntry; diff --git a/client/admin/apps/LogItem.tsx b/client/admin/apps/LogItem.tsx new file mode 100644 index 00000000000..abeebb21eab --- /dev/null +++ b/client/admin/apps/LogItem.tsx @@ -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 = ({ entries, instanceId, title, ...props }) => { + const t = useTranslation(); + + return + {instanceId && {t('Instance')}: {instanceId}} + {entries.map(({ severity, timestamp, caller, args }, i) => )} + ; +}; + +export default LogItem; diff --git a/client/admin/apps/LogsLoading.tsx b/client/admin/apps/LogsLoading.tsx new file mode 100644 index 00000000000..dd194b72baf --- /dev/null +++ b/client/admin/apps/LogsLoading.tsx @@ -0,0 +1,13 @@ +import { Box, Skeleton, Margins } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +const LogsLoading: FC = () => + + + + + + + ; + +export default LogsLoading; diff --git a/client/admin/apps/MarketplaceRow.tsx b/client/admin/apps/MarketplaceRow.tsx new file mode 100644 index 00000000000..ef3ccc90bfd --- /dev/null +++ b/client/admin/apps/MarketplaceRow.tsx @@ -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 = ({ + 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): void => { + if (!['Enter', 'Space'].includes(e.nativeEvent.code)) { + return; + } + + handleClick(); + }; + + const preventClickPropagation = (e: MouseEvent): void => { + e.stopPropagation(); + }; + + return setFocused(true)} + onBlur={(): void => setFocused(false)} + onMouseEnter={(): void => setHovered(true)} + onMouseLeave={(): void => setHovered(false)} + > + + + + {name} + {`${ t('By') } ${ authorName }`} + + + {large && + + {description} + {categories && + {categories.map((current) => {current})} + } + + } + {medium && + + } + + + + {installed && } + + + ; +}; + +export default memo(MarketplaceRow); diff --git a/client/admin/apps/MarketplaceTable.js b/client/admin/apps/MarketplaceTable.js index 8e7f6a6936e..0b38acba91d 100644 --- a/client/admin/apps/MarketplaceTable.js +++ b/client/admin/apps/MarketplaceTable.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}); - -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 setFocused(true)} - onBlur={() => setFocused(false)} - onMouseEnter={() => setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - - - - {name} - {`${ t('By') } ${ authorName }`} - - - {large && - - {description} - {categories && - {categories.map((current) => {current})} - } - - } - {medium && - - } - - - - {installed && } - - - ; -}); +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 }) => } > {(props) => { 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 {!showType && type === 'Free' ? t(type) : formatedPrice} ; } + +export default PriceDisplay; diff --git a/client/admin/apps/SettingsDisplay.tsx b/client/admin/apps/SettingsDisplay.tsx new file mode 100644 index 00000000000..9b92ab15c40 --- /dev/null +++ b/client/admin/apps/SettingsDisplay.tsx @@ -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>; +}; + +const SettingsDisplay: FC = ({ + 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 <> + + + {t('Settings')} + + + ; +}; + +export default SettingsDisplay; diff --git a/client/admin/apps/types.ts b/client/admin/apps/types.ts new file mode 100644 index 00000000000..dfddeeefb97 --- /dev/null +++ b/client/admin/apps/types.ts @@ -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; +}; diff --git a/client/admin/cloud/CopyStep.tsx b/client/admin/cloud/CopyStep.tsx new file mode 100644 index 00000000000..063fa7bfd68 --- /dev/null +++ b/client/admin/cloud/CopyStep.tsx @@ -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 = ({ onNextButtonClick }) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [clientKey, setClientKey] = useState(''); + + const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData'); + + useEffect(() => { + const loadWorkspaceRegisterData = async (): Promise => { + const clientKey = await getWorkspaceRegisterData(); + setClientKey(clientKey); + }; + + loadWorkspaceRegisterData(); + }, [getWorkspaceRegisterData]); + + const copyRef = useRef(); + + 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 <> + + +

{t('Cloud_register_offline_helper')}

+
+ + + + {clientKey} + + + + + +
+ + + + + + ; +}; + +export default CopyStep; diff --git a/client/admin/cloud/ManualWorkspaceRegistrationModal.js b/client/admin/cloud/ManualWorkspaceRegistrationModal.js index d6e730de1b6..56296da4060 100644 --- a/client/admin/cloud/ManualWorkspaceRegistrationModal.js +++ b/client/admin/cloud/ManualWorkspaceRegistrationModal.js @@ -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 <> - - -

{t('Cloud_register_offline_helper')}

-
- - - - {clientKey} - - - - - -
- - - - - - ; -} - -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 <> - - -

{t('Cloud_register_offline_finish_helper')}

-
- - - - - -
- - - - - - - ; -} +import CopyStep from './CopyStep'; +import PasteStep from './PasteStep'; const Steps = { COPY: 'copy', diff --git a/client/admin/cloud/PasteStep.tsx b/client/admin/cloud/PasteStep.tsx new file mode 100644 index 00000000000..21e8dd287ce --- /dev/null +++ b/client/admin/cloud/PasteStep.tsx @@ -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 = ({ onBackButtonClick, onFinish }) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [isLoading, setLoading] = useState(false); + const [cloudKey, setCloudKey] = useState(''); + + const handleCloudKeyChange = (e: ChangeEvent): void => { + setCloudKey(e.currentTarget.value); + }; + + const registerManually = useEndpoint('POST', 'cloud.manualRegister'); + + const handleFinishButtonClick = async (): Promise => { + 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 <> + + +

{t('Cloud_register_offline_finish_helper')}

+
+ + + + + +
+ + + + + + + ; +}; + +export default PasteStep; diff --git a/client/admin/customEmoji/AddCustomEmoji.js b/client/admin/customEmoji/AddCustomEmoji.js index 86585a462d3..b2ec21e244e 100644 --- a/client/admin/customEmoji/AddCustomEmoji.js +++ b/client/admin/customEmoji/AddCustomEmoji.js @@ -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 }) { ; } + +export default AddCustomEmoji; diff --git a/client/admin/customEmoji/CustomEmoji.js b/client/admin/customEmoji/CustomEmoji.js index 3effa9b5cc2..f739dcb7ae0 100644 --- a/client/admin/customEmoji/CustomEmoji.js +++ b/client/admin/customEmoji/CustomEmoji.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}; - -export function CustomEmoji({ +function CustomEmoji({ data, sort, onClick, @@ -28,8 +16,8 @@ export function CustomEmoji({ const t = useTranslation(); const header = useMemo(() => [ - {t('Name')}, - {t('Aliases')}, + {t('Name')}, + {t('Aliases')}, ], [onHeaderClick, sort, t]); const renderRow = (emojis) => { @@ -40,5 +28,15 @@ export function CustomEmoji({ ; }; - return ; + return } + />; } + +export default CustomEmoji; diff --git a/client/admin/customEmoji/CustomEmoji.stories.js b/client/admin/customEmoji/CustomEmoji.stories.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/client/admin/customEmoji/CustomEmojiRoute.js b/client/admin/customEmoji/CustomEmojiRoute.js index 646b22a0856..86ecb1e2ae1 100644 --- a/client/admin/customEmoji/CustomEmojiRoute.js +++ b/client/admin/customEmoji/CustomEmojiRoute.js @@ -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 }) { } ; } + + +export default CustomEmojiRoute; diff --git a/client/admin/customEmoji/EditCustomEmoji.js b/client/admin/customEmoji/EditCustomEmoji.tsx similarity index 53% rename from client/admin/customEmoji/EditCustomEmoji.js rename to client/admin/customEmoji/EditCustomEmoji.tsx index 63c7c0d9991..2ae34caac4b 100644 --- a/client/admin/customEmoji/EditCustomEmoji.js +++ b/client/admin/customEmoji/EditCustomEmoji.tsx @@ -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 - - - {t('Are_you_sure')} - - - - {t('Custom_Emoji_Delete_Warning')} - - - - - - - - ; -}; -const SuccessModal = ({ onClose, ...props }) => { - const t = useTranslation(); - return - - - {t('Deleted')} - - - - {t('Custom_Emoji_Has_Been_Deleted')} - - - - - - - ; +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 - - - - - - - - - - - - ; - } - - if (error || !data || !data.emojis || data.emojis.update.length < 1) { - return {t('Custom_User_Status_Error_Invalid_User_Status')}; - } - - return ; -} - -export function EditCustomEmoji({ close, onChange, data, ...props }) { +const EditCustomEmoji: FC = ({ 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(); 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(() => { setModal(undefined); close(); onChange(); }}/>); + setModal(() => { setModal(undefined); close(); onChange(); }} + />); } - }, [close, deleteAction, onChange]); + }, [close, deleteAction, onChange, setModal, t]); - const openConfirmDelete = useCallback(() => setModal(() => setModal(undefined)}/>), [onDeleteConfirm, setModal]); + const openConfirmDelete = useCallback(() => setModal(() => setModal(undefined)} + />), [onDeleteConfirm, setModal, t]); const handleAliasesChange = useCallback((e) => setAliases(e.currentTarget.value), [setAliases]); const [clickUpload] = useFileInput(setEmojiPreview, 'emoji'); - return + return {t('Name')} - setName(e.currentTarget.value)} placeholder={t('Name')} /> + ): void => setName(e.currentTarget.value)} placeholder={t('Name')} /> @@ -173,4 +122,6 @@ export function EditCustomEmoji({ close, onChange, data, ...props }) { ; -} +}; + +export default EditCustomEmoji; diff --git a/client/admin/customEmoji/EditCustomEmojiWithData.tsx b/client/admin/customEmoji/EditCustomEmojiWithData.tsx new file mode 100644 index 00000000000..f8b442a4721 --- /dev/null +++ b/client/admin/customEmoji/EditCustomEmojiWithData.tsx @@ -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 = ({ _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 + + + + + + + + + + + + ; + } + + if (error || !data || !data.emojis || data.emojis.update.length < 1) { + return {t('Custom_User_Status_Error_Invalid_User_Status')}; + } + + return ; +}; + +export default EditCustomEmojiWithData; diff --git a/client/admin/customEmoji/types.ts b/client/admin/customEmoji/types.ts new file mode 100644 index 00000000000..d8a0c7c4a36 --- /dev/null +++ b/client/admin/customEmoji/types.ts @@ -0,0 +1,6 @@ +export type EmojiDescriptor = { + _id: string; + name: string; + aliases: string[]; + extension: string; +}; diff --git a/client/admin/customSounds/AddCustomSound.js b/client/admin/customSounds/AddCustomSound.js index e87a314a891..14734c026d5 100644 --- a/client/admin/customSounds/AddCustomSound.js +++ b/client/admin/customSounds/AddCustomSound.js @@ -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 }) { ; } + +export default AddCustomSound; diff --git a/client/admin/customSounds/AdminSounds.js b/client/admin/customSounds/AdminSounds.js index 031ccb3a58f..720b28cee51 100644 --- a/client/admin/customSounds/AdminSounds.js +++ b/client/admin/customSounds/AdminSounds.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}; - -export function AdminSounds({ +function AdminSounds({ data, sort, onClick, @@ -29,8 +17,8 @@ export function AdminSounds({ const t = useTranslation(); const header = useMemo(() => [ - {t('Name')}, - , + {t('Name')}, + , ], [sort]); const customSound = useCustomSound(); @@ -52,5 +40,15 @@ export function AdminSounds({ ; }; - return ; + return } + />; } + +export default AdminSounds; diff --git a/client/admin/customSounds/AdminSoundsRoute.js b/client/admin/customSounds/AdminSoundsRoute.js index af3f832fe14..33c9ec20aaf 100644 --- a/client/admin/customSounds/AdminSoundsRoute.js +++ b/client/admin/customSounds/AdminSoundsRoute.js @@ -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'; diff --git a/client/admin/customSounds/EditCustomSound.js b/client/admin/customSounds/EditCustomSound.js index dd755a9e7f3..c123f50ab07 100644 --- a/client/admin/customSounds/EditCustomSound.js +++ b/client/admin/customSounds/EditCustomSound.js @@ -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 - - - {t('Are_you_sure')} - - - - {t('Custom_Sound_Delete_Warning')} - - - - - - - - ; -}; - -const SuccessModal = ({ onClose, ...props }) => { - const t = useTranslation(); - return - - - {t('Deleted')} - - - - {t('Custom_Sound_Has_Been_Deleted')} - - - - - - - ; -}; - -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(() => { setModal(undefined); close(); onChange(); }}/>); + setModal(() => { setModal(undefined); close(); onChange(); }} + />); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); onChange(); } }, [_id, close, deleteCustomSound, dispatchToastMessage, onChange]); - const openConfirmDelete = () => setModal(() => setModal(undefined)}/>); + const openConfirmDelete = () => setModal(() => setModal(undefined)} + />); const [clickUpload] = useFileInput(handleChangeFile, 'audio/mp3'); @@ -192,3 +162,5 @@ function EditSound({ close, onChange, data, ...props }) { ; } + +export default EditCustomSound; diff --git a/client/admin/customUserStatus/AddCustomUserStatus.js b/client/admin/customUserStatus/AddCustomUserStatus.js index 573d50d4c82..72f27cabf20 100644 --- a/client/admin/customUserStatus/AddCustomUserStatus.js +++ b/client/admin/customUserStatus/AddCustomUserStatus.js @@ -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 }) { ; } + +export default AddCustomUserStatus; diff --git a/client/admin/customUserStatus/CustomUserStatus.js b/client/admin/customUserStatus/CustomUserStatus.js index 743a880490d..07d27d4643f 100644 --- a/client/admin/customUserStatus/CustomUserStatus.js +++ b/client/admin/customUserStatus/CustomUserStatus.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}; - -export function CustomUserStatus({ +function CustomUserStatus({ data, sort, onClick, @@ -30,8 +18,8 @@ export function CustomUserStatus({ const t = useTranslation(); const header = useMemo(() => [ - {t('Name')}, - {t('Presence')}, + {t('Name')}, + {t('Presence')}, ].filter(Boolean), [onHeaderClick, sort, t]); const renderRow = (status) => { @@ -42,5 +30,15 @@ export function CustomUserStatus({ ; }; - return ; + return } + />; } + +export default CustomUserStatus; diff --git a/client/admin/customUserStatus/CustomUserStatusRoute.js b/client/admin/customUserStatus/CustomUserStatusRoute.js index d4a1a24bad7..499045f0905 100644 --- a/client/admin/customUserStatus/CustomUserStatusRoute.js +++ b/client/admin/customUserStatus/CustomUserStatusRoute.js @@ -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 }) { } ; } + +export default CustomUserStatusRoute; diff --git a/client/admin/customUserStatus/EditCustomUserStatus.js b/client/admin/customUserStatus/EditCustomUserStatus.js index c8a82e3bf33..6e222ac2e29 100644 --- a/client/admin/customUserStatus/EditCustomUserStatus.js +++ b/client/admin/customUserStatus/EditCustomUserStatus.js @@ -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 - - - {t('Are_you_sure')} - - - - {t('Custom_User_Status_Delete_Warning')} - - - - - - - - ; -}; - -const SuccessModal = ({ onClose, ...props }) => { - const t = useTranslation(); - return - - - {t('Deleted')} - - - - {t('Custom_User_Status_Has_Been_Deleted')} - - - - - - - ; -}; - -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 - - - - - - - - - - - - ; - } - - if (error || !data || data.statuses.length < 1) { - return {t('Custom_User_Status_Error_Invalid_User_Status')}; - } - - return ; -} +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(() => { setModal(undefined); close(); onChange(); }}/>); + setModal(() => { setModal(undefined); close(); onChange(); }} + />); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); onChange(); } }, [_id, close, deleteStatus, dispatchToastMessage, onChange]); - const openConfirmDelete = () => setModal(() => setModal(undefined)}/>); + const openConfirmDelete = () => setModal(() => setModal(undefined)} + />); const presenceOptions = [ ['online', t('Online')], @@ -163,3 +100,5 @@ export function EditCustomUserStatus({ close, onChange, data, ...props }) { ; } + +export default EditCustomUserStatus; diff --git a/client/admin/customUserStatus/EditCustomUserStatusWithData.tsx b/client/admin/customUserStatus/EditCustomUserStatusWithData.tsx new file mode 100644 index 00000000000..71d2b9ca6c6 --- /dev/null +++ b/client/admin/customUserStatus/EditCustomUserStatusWithData.tsx @@ -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 = ({ _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 + + + + + + + + + + + + ; + } + + if (error || !data || data.statuses.length < 1) { + return {t('Custom_User_Status_Error_Invalid_User_Status')}; + } + + return ; +}; + +export default EditCustomUserStatusWithData; diff --git a/client/admin/federationDashboard/FederationDashboardRoute.tsx b/client/admin/federationDashboard/FederationDashboardRoute.tsx index a0beddef26f..458f6e22ca1 100644 --- a/client/admin/federationDashboard/FederationDashboardRoute.tsx +++ b/client/admin/federationDashboard/FederationDashboardRoute.tsx @@ -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) { diff --git a/client/admin/federationDashboard/OverviewSection.js b/client/admin/federationDashboard/OverviewSection.js index 328948e31d6..21bca7d48b8 100644 --- a/client/admin/federationDashboard/OverviewSection.js +++ b/client/admin/federationDashboard/OverviewSection.js @@ -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 && ) || (overviewStatus === AsyncState.ERROR && Error) diff --git a/client/admin/federationDashboard/ServersSection.js b/client/admin/federationDashboard/ServersSection.js index d679b9fc988..dbacc976ac7 100644 --- a/client/admin/federationDashboard/ServersSection.js +++ b/client/admin/federationDashboard/ServersSection.js @@ -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 ; diff --git a/client/admin/import/PrepareChannels.tsx b/client/admin/import/PrepareChannels.tsx new file mode 100644 index 00000000000..13963ff3a6a --- /dev/null +++ b/client/admin/import/PrepareChannels.tsx @@ -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>; +}; + +const PrepareChannels: FC = ({ 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 <> + + + + + 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 })); + }); + }} + /> + + {t('Name')} + + + + + {channels.slice(current, current + itemsPerPage).map((channel) => + + ): void => { + const { checked } = event.currentTarget; + setChannels((channels) => + channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel))); + }} + /> + + {channel.name} + {channel.is_archived && {t('Importer_Archived')}} + )} + +
+ + ; +}; + +export default PrepareChannels; diff --git a/client/admin/import/PrepareImportPage.js b/client/admin/import/PrepareImportPage.js index 298bb402782..86ca218bd43 100644 --- a/client/admin/import/PrepareImportPage.js +++ b/client/admin/import/PrepareImportPage.js @@ -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 <> - - - - - 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 })); - }); - }} - /> - - {t('Username')} - {t('Email')} - - - - - {users.slice(current, current + itemsPerPage).map((user) => - - { - const { checked } = event.currentTarget; - setUsers((users) => - users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user))); - }} - /> - - {user.username} - {user.email} - {user.is_deleted && {t('Deleted')}} - )} - -
- - ; -} - -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 && <> - - - - 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 })); - }); - }} - /> - - {t('Name')} - - - - - {channels.slice(current, current + itemsPerPage).map((channel) => - - { - const { checked } = event.currentTarget; - setChannels((channels) => - channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel))); - }} - /> - - {channel.name} - {channel.is_archived && {t('Importer_Archived')}} - )} - -
- ; -} - function PrepareImportPage() { const t = useTranslation(); const handleError = useErrorHandler(); diff --git a/client/admin/import/PrepareUsers.tsx b/client/admin/import/PrepareUsers.tsx new file mode 100644 index 00000000000..03e7a51508a --- /dev/null +++ b/client/admin/import/PrepareUsers.tsx @@ -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>; +}; + +const PrepareUsers: FC = ({ 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 <> + + + + + 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 })); + }); + }} + /> + + {t('Username')} + {t('Email')} + + + + + {users.slice(current, current + itemsPerPage).map((user) => + + ): void => { + const { checked } = event.currentTarget; + setUsers((users) => + users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user))); + }} + /> + + {user.username} + {user.email} + {user.is_deleted && {t('Deleted')}} + )} + +
+ + ; +}; + +export default PrepareUsers; diff --git a/client/admin/info/BuildEnvironmentSection.js b/client/admin/info/BuildEnvironmentSection.js index 520a9ae1c61..40bafc12c9a 100644 --- a/client/admin/info/BuildEnvironmentSection.js +++ b/client/admin/info/BuildEnvironmentSection.js @@ -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 {formatDateAndTime(build.date)} ; }); + +export default BuildEnvironmentSection; diff --git a/client/admin/info/BuildEnvironmentSection.stories.js b/client/admin/info/BuildEnvironmentSection.stories.js index b7cabc5630a..58c97a8efca 100644 --- a/client/admin/info/BuildEnvironmentSection.stories.js +++ b/client/admin/info/BuildEnvironmentSection.stories.js @@ -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', diff --git a/client/admin/info/CommitSection.js b/client/admin/info/CommitSection.js index 2c1ee4b9216..94773a9d9d1 100644 --- a/client/admin/info/CommitSection.js +++ b/client/admin/info/CommitSection.js @@ -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 }) { {commit.subject} ; }); + +export default CommitSection; diff --git a/client/admin/info/CommitSection.stories.js b/client/admin/info/CommitSection.stories.js index 80f1d1133fc..d8b0de4df48 100644 --- a/client/admin/info/CommitSection.stories.js +++ b/client/admin/info/CommitSection.stories.js @@ -1,6 +1,6 @@ import React from 'react'; -import { CommitSection } from './CommitSection'; +import CommitSection from './CommitSection'; export default { title: 'admin/info/CommitSection', diff --git a/client/admin/info/DescriptionList.js b/client/admin/info/DescriptionList.js index a71d4cb651b..7c59c77dd6a 100644 --- a/client/admin/info/DescriptionList.js +++ b/client/admin/info/DescriptionList.js @@ -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 && {title} } @@ -14,10 +14,12 @@ export const DescriptionList = React.memo(({ children, title, ...props }) => <> ); -const Entry = ({ children, label, ...props }) => +const DescriptionListEntry = ({ children, label, ...props }) => {label} {children} ; -DescriptionList.Entry = React.memo(Entry); +DescriptionList.Entry = React.memo(DescriptionListEntry); + +export default DescriptionList; diff --git a/client/admin/info/DescriptionList.stories.js b/client/admin/info/DescriptionList.stories.js index 4571c2476db..5673ed85265 100644 --- a/client/admin/info/DescriptionList.stories.js +++ b/client/admin/info/DescriptionList.stories.js @@ -1,6 +1,6 @@ import React from 'react'; -import { DescriptionList } from './DescriptionList'; +import DescriptionList from './DescriptionList'; import Page from '../../components/basic/Page'; export default { diff --git a/client/admin/info/InformationPage.js b/client/admin/info/InformationPage.js index 09ce38c3229..7430723da1f 100644 --- a/client/admin/info/InformationPage.js +++ b/client/admin/info/InformationPage.js @@ -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({ ; }); + +export default InformationPage; diff --git a/client/admin/info/InformationPage.stories.js b/client/admin/info/InformationPage.stories.js index fee5326ea4d..7ad495ee8f3 100644 --- a/client/admin/info/InformationPage.stories.js +++ b/client/admin/info/InformationPage.stories.js @@ -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', diff --git a/client/admin/info/InformationRoute.js b/client/admin/info/InformationRoute.js index 024b9a1c106..4f3ae4aba00 100644 --- a/client/admin/info/InformationRoute.js +++ b/client/admin/info/InformationRoute.js @@ -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([]); diff --git a/client/admin/info/InstancesSection.js b/client/admin/info/InstancesSection.js index d0138a84414..127f2a09f3e 100644 --- a/client/admin/info/InstancesSection.js +++ b/client/admin/info/InstancesSection.js @@ -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; diff --git a/client/admin/info/InstancesSection.stories.js b/client/admin/info/InstancesSection.stories.js index 948f197d3f8..64ef70086b1 100644 --- a/client/admin/info/InstancesSection.stories.js +++ b/client/admin/info/InstancesSection.stories.js @@ -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', diff --git a/client/admin/info/RocketChatSection.js b/client/admin/info/RocketChatSection.js index 0f4c93356b7..7b281361b97 100644 --- a/client/admin/info/RocketChatSection.js +++ b/client/admin/info/RocketChatSection.js @@ -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 {s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))} ; }); + +export default RocketChatSection; diff --git a/client/admin/info/RocketChatSection.stories.js b/client/admin/info/RocketChatSection.stories.js index 71046e11ba9..5410f9132a0 100644 --- a/client/admin/info/RocketChatSection.stories.js +++ b/client/admin/info/RocketChatSection.stories.js @@ -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', diff --git a/client/admin/info/RuntimeEnvironmentSection.js b/client/admin/info/RuntimeEnvironmentSection.js index 65dde9da950..609c8335244 100644 --- a/client/admin/info/RuntimeEnvironmentSection.js +++ b/client/admin/info/RuntimeEnvironmentSection.js @@ -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 ? : fn()); const t = useTranslation(); const formatMemorySize = useFormatMemorySize(); @@ -41,3 +41,5 @@ export const RuntimeEnvironmentSection = React.memo(function RuntimeEnvironmentS {s(() => statistics.os.cpus.length)} ; }); + +export default RuntimeEnvironmentSection; diff --git a/client/admin/info/RuntimeEnvironmentSection.stories.js b/client/admin/info/RuntimeEnvironmentSection.stories.js index 3270bbfe081..641644c4d6d 100644 --- a/client/admin/info/RuntimeEnvironmentSection.stories.js +++ b/client/admin/info/RuntimeEnvironmentSection.stories.js @@ -1,6 +1,6 @@ import React from 'react'; -import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection'; +import RuntimeEnvironmentSection from './RuntimeEnvironmentSection'; export default { title: 'admin/info/RuntimeEnvironmentSection', diff --git a/client/admin/info/UsageSection.js b/client/admin/info/UsageSection.js index 4528cd21303..e4fa011bf4d 100644 --- a/client/admin/info/UsageSection.js +++ b/client/admin/info/UsageSection.js @@ -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 ? : fn()); const formatMemorySize = useFormatMemorySize(); const t = useTranslation(); @@ -50,3 +50,5 @@ export const UsageSection = React.memo(function UsageSection({ statistics, isLoa {s(() => statistics.integrations.totalWithScriptEnabled)} ; }); + +export default UsageSection; diff --git a/client/admin/info/UsageSection.stories.js b/client/admin/info/UsageSection.stories.js index 81653490717..5f7c6c205dd 100644 --- a/client/admin/info/UsageSection.stories.js +++ b/client/admin/info/UsageSection.stories.js @@ -1,6 +1,6 @@ import React from 'react'; -import { UsageSection } from './UsageSection'; +import UsageSection from './UsageSection'; export default { title: 'admin/info/UsageSection', diff --git a/client/admin/integrations/IntegrationsTable.js b/client/admin/integrations/IntegrationsTable.js index 8efe505a287..ccc16945259 100644 --- a/client/admin/integrations/IntegrationsTable.js +++ b/client/admin/integrations/IntegrationsTable.js @@ -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(() => [ - {t('Name')}, - {t('Post_to')}, - {t('Created_by')}, - isBig && {t('Created_at')}, - {t('Post_as')}, + {t('Name')}, + {t('Post_to')}, + {t('Created_by')}, + isBig && {t('Created_at')}, + {t('Post_as')}, ].filter(Boolean), [sort, onHeaderClick, isBig, t]); const renderRow = useCallback((props) => , [isBig, onClick]); - return ; + return } + />; } export default IntegrationsTable; diff --git a/client/admin/integrations/edit/EditIncomingWebhook.js b/client/admin/integrations/edit/EditIncomingWebhook.js index 3935abb6ec1..0acdee03c58 100644 --- a/client/admin/integrations/edit/EditIncomingWebhook.js +++ b/client/admin/integrations/edit/EditIncomingWebhook.js @@ -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( { closeModal(); router.push({}); }}/>); } + if (result.success) { + setModal( { closeModal(); router.push({}); }} + />); + } }; - setModal(); - }, [deleteIntegration, router]); + setModal(); + }, [deleteIntegration, router, setModal, t]); const handleSave = useCallback(async () => { try { diff --git a/client/admin/integrations/edit/EditIntegrationsPage.js b/client/admin/integrations/edit/EditIntegrationsPage.js index 9547a6de9d9..3ba398affac 100644 --- a/client/admin/integrations/edit/EditIntegrationsPage.js +++ b/client/admin/integrations/edit/EditIntegrationsPage.js @@ -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 - - - {t('Are_you_sure')} - - - - {t('Integration_Delete_Warning')} - - - - - - - - ; -}; - -export const SuccessModal = ({ onClose, ...props }) => { - const t = useTranslation(); - return - - - {t('Deleted')} - - - - {t('Your_entry_has_been_deleted')} - - - - - - - ; -}; - export default function EditIntegrationsPage({ ...props }) { const t = useTranslation(); diff --git a/client/admin/integrations/edit/EditOutgoingWebhook.js b/client/admin/integrations/edit/EditOutgoingWebhook.js index 32d8b9d8ffd..2d402a685d3 100644 --- a/client/admin/integrations/edit/EditOutgoingWebhook.js +++ b/client/admin/integrations/edit/EditOutgoingWebhook.js @@ -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( { closeModal(); router.push({}); }}/>); } + if (result.success) { + setModal( { closeModal(); router.push({}); }} + />); + } }; - setModal(); - }, [deleteIntegration, router]); + setModal(); + }, [deleteIntegration, router, setModal, t]); const { urls, diff --git a/client/admin/invites/InvitesPage.js b/client/admin/invites/InvitesPage.js index 682dd44e34f..f53fe10c34a 100644 --- a/client/admin/invites/InvitesPage.js +++ b/client/admin/invites/InvitesPage.js @@ -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'; diff --git a/client/admin/mailer/MailerRoute.js b/client/admin/mailer/MailerRoute.js index 1e7d076d43a..4e41ac27a78 100644 --- a/client/admin/mailer/MailerRoute.js +++ b/client/admin/mailer/MailerRoute.js @@ -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'), + }); }; }; diff --git a/client/admin/oauthApps/OAuthAppsTable.js b/client/admin/oauthApps/OAuthAppsTable.js index ad827da2fde..258eb1e2753 100644 --- a/client/admin/oauthApps/OAuthAppsTable.js +++ b/client/admin/oauthApps/OAuthAppsTable.js @@ -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(() => [ - {t('Name')}, - {t('Created_by')}, - {t('Created_at')}, + {t('Name')}, + {t('Created_by')}, + {t('Created_at')}, ], [t]); const renderRow = useCallback(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) => diff --git a/client/admin/oauthApps/OAuthEditApp.js b/client/admin/oauthApps/OAuthEditApp.js index b24bd75a311..4d346d2ff0a 100644 --- a/client/admin/oauthApps/OAuthEditApp.js +++ b/client/admin/oauthApps/OAuthEditApp.js @@ -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 - - - {t('Are_you_sure')} - - - - {t('Application_delete_warning')} - - - - - - - - ; -}; - -const SuccessModal = ({ onClose, ...props }) => { - const t = useTranslation(); - return - - - {t('Deleted')} - - - - {t('Your_entry_has_been_deleted')} - - - - - - - ; -}; +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(() => { setModal(); close(); }}/>); + setModal(() => { setModal(); close(); }} + />); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - }, [close, data._id, deleteApp, dispatchToastMessage]); + }, [close, data._id, deleteApp, dispatchToastMessage, setModal, t]); - const openConfirmDelete = () => setModal(() => setModal(undefined)}/>); + const openConfirmDelete = () => setModal(() => setModal(undefined)} + />); const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) }); diff --git a/client/admin/permissions/PermissionsTable.js b/client/admin/permissions/PermissionsTable.js index d6f41a688d8..45071562591 100644 --- a/client/admin/permissions/PermissionsTable.js +++ b/client/admin/permissions/PermissionsTable.js @@ -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'; diff --git a/client/admin/permissions/UsersInRoleTable.js b/client/admin/permissions/UsersInRoleTable.js index df255667d7d..016779f3440 100644 --- a/client/admin/permissions/UsersInRoleTable.js +++ b/client/admin/permissions/UsersInRoleTable.js @@ -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( - {t('The_user_s_will_be_removed_from_role_s', username, roleName)} - ); + setModal(); }); return [ - {t('Name')}, - {t('Type')}, - {t('Users')}, - mediaQuery && {t('Msgs')}, - mediaQuery && {t('Default')}, - mediaQuery && {t('Featured')}, + {t('Name')}, + {t('Type')}, + {t('Users')}, + mediaQuery && {t('Msgs')}, + mediaQuery && {t('Default')}, + mediaQuery && {t('Featured')}, ].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() { ; }, [mediaQuery, onClick, t]); - return ; + return } + />; } export default RoomsTable; diff --git a/client/admin/sidebar/AdminSidebar.js b/client/admin/sidebar/AdminSidebar.js index 2ea9ff38571..de01dc0671c 100644 --- a/client/admin/sidebar/AdminSidebar.js +++ b/client/admin/sidebar/AdminSidebar.js @@ -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 - - ; -}); - -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 - {t('Settings')} - - } - /> - - - {isLoadingGroups && } - {!isLoadingGroups && !!groups.length && ({ - name: t(group.i18nLabel || group._id), - pathSection: 'admin', - pathGroup: group._id, - }))} - currentPath={currentPath} - />} - {!isLoadingGroups && !groups.length && {t('Nothing_found')}} - - ; -}; - -export default React.memo(function AdminSidebar() { +function AdminSidebar() { const t = useTranslation(); const canViewSettings = useAtLeastOnePermission( @@ -140,4 +50,6 @@ export default React.memo(function AdminSidebar() { ; -}); +} + +export default memo(AdminSidebar); diff --git a/client/admin/sidebar/AdminSidebarPages.tsx b/client/admin/sidebar/AdminSidebarPages.tsx new file mode 100644 index 00000000000..9c8d8fe2b84 --- /dev/null +++ b/client/admin/sidebar/AdminSidebarPages.tsx @@ -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 = ({ currentPath }) => { + const items = useSubscription(itemsSubscription); + + return + + ; +}; + +export default memo(AdminSidebarPages); diff --git a/client/admin/sidebar/AdminSidebarSettings.tsx b/client/admin/sidebar/AdminSidebarSettings.tsx new file mode 100644 index 00000000000..fd88b0554d3 --- /dev/null +++ b/client/admin/sidebar/AdminSidebarSettings.tsx @@ -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 = ({ 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 + {t('Settings')} + + } + /> + + + {isLoadingGroups && } + {!isLoadingGroups && !!groups.length && ({ + name: t(group.i18nLabel || group._id), + pathSection: 'admin', + pathGroup: group._id, + }))} + currentPath={currentPath} + />} + {!isLoadingGroups && !groups.length && {t('Nothing_found')}} + + ; +}; + +export default AdminSidebarSettings; diff --git a/client/admin/users/UserInfoActions.js b/client/admin/users/UserInfoActions.js index 350030a2fe0..4d8bc1a276f 100644 --- a/client/admin/users/UserInfoActions.js +++ b/client/admin/users/UserInfoActions.js @@ -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 - - - {t('Are_you_sure')} - - - - {text} - - - - - - - - ; -}; - -const SuccessModal = ({ onClose, title, text, ...props }) => { - const t = useTranslation(); - return - - - {title} - - - - {text} - - - - - - - ; -}; +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( { setModal(); onChange(); }}/>); + setModal( { setModal(); onChange(); }} + />); } else { setModal(); } @@ -113,7 +78,11 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) }); const confirmDeleteUser = useCallback(() => { - setModal( setModal()} text={t(`Delete_User_Warning_${ erasureType }`)} confirmText={t('Delete')} />); + setModal( 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( { setModal(); onChange(); }}/>); + setModal( { setModal(); onChange(); }} + />); } }, [resetE2EEKeyRequest, onChange, setModal, t, _id]); const confirmResetE2EEKey = useCallback(() => { - setModal( setModal()} text={t('E2E_Reset_Other_Key_Warning')} confirmText={t('Reset')} />); + setModal( setModal()} + onDelete={resetE2EEKey} + />); }, [resetE2EEKey, t, setModal]); const activeStatusQuery = useMemo(() => ({ diff --git a/client/admin/users/UsersTable.js b/client/admin/users/UsersTable.js index 6caa9ff1020..2f9cf13c5ac 100644 --- a/client/admin/users/UsersTable.js +++ b/client/admin/users/UsersTable.js @@ -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 e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; +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 {t('Name')} @@ -120,6 +111,7 @@ export function UsersTable() { total={data.total} setParams={setParams} params={params} + renderFilter={({ onChange, ...props }) => } > {(props) => } ; diff --git a/client/channel/ExportMessages/index.js b/client/channel/ExportMessages/index.js index 93b0e06dfef..e793a125307 100644 --- a/client/channel/ExportMessages/index.js +++ b/client/channel/ExportMessages/index.js @@ -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 }) => { - { errorMessage && } + {errorMessage && } + + + ; +}; + +export default DeleteSuccessModal; diff --git a/client/components/DeleteWarningModal.js b/client/components/DeleteWarningModal.js deleted file mode 100644 index a82bf473367..00000000000 --- a/client/components/DeleteWarningModal.js +++ /dev/null @@ -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 - - - {t('Are_you_sure')} - - - - {children} - - - - - - - - ; -}; - -export default DeleteWarningModal; diff --git a/client/components/DeleteWarningModal.tsx b/client/components/DeleteWarningModal.tsx new file mode 100644 index 00000000000..94e1c0c7fad --- /dev/null +++ b/client/components/DeleteWarningModal.tsx @@ -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 = ({ + children, + cancelText, + deleteText, + onCancel, + onDelete, + ...props +}) => { + const t = useTranslation(); + + return + + + {t('Are_you_sure')} + + + + {children} + + + + + + + + ; +}; + +export default DeleteWarningModal; diff --git a/client/components/FilterByText.tsx b/client/components/FilterByText.tsx new file mode 100644 index 00000000000..54fa2113f33 --- /dev/null +++ b/client/components/FilterByText.tsx @@ -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 = ({ + placeholder, + onChange: setFilter, + ...props +}) => { + const t = useTranslation(); + + const [text, setText] = useState(''); + + const handleInputChange = useCallback((event: ChangeEvent) => { + setText(event.currentTarget.value); + }, []); + + useEffect(() => { + setFilter({ text }); + }, [setFilter, text]); + + const handleFormSubmit = useCallback((event: FormEvent) => { + event.preventDefault(); + }, []); + + return + } onChange={handleInputChange} value={text} /> + ; +}; + +export default memo(FilterByText); diff --git a/client/components/GenericTable.stories.js b/client/components/GenericTable.stories.js index 678ae1831ee..7378fade991 100644 --- a/client/components/GenericTable.stories.js +++ b/client/components/GenericTable.stories.js @@ -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 = [ - Name, - Email, - Data, - Info, + Name, + Email, + Data, + Info, ]; - return ; + return } + />; }; diff --git a/client/components/GenericTable/HeaderCell.tsx b/client/components/GenericTable/HeaderCell.tsx new file mode 100644 index 00000000000..7783e27900e --- /dev/null +++ b/client/components/GenericTable/HeaderCell.tsx @@ -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 = ({ + children, + active, + direction, + sort, + onClick, + ...props +}) => { + const fn = useCallback(() => onClick && onClick(!!sort), [sort, onClick]); + return + + {children} + {sort && } + + ; +}; + +export default HeaderCell; diff --git a/client/components/GenericTable/LoadingRow.tsx b/client/components/GenericTable/LoadingRow.tsx new file mode 100644 index 00000000000..70324a21180 --- /dev/null +++ b/client/components/GenericTable/LoadingRow.tsx @@ -0,0 +1,24 @@ +import { Box, Skeleton, Table } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +type LoadingRowProps = { + cols: number; +}; + +const LoadingRow: FC = ({ cols }) => + + + + + + + + + + + {Array.from({ length: cols - 1 }, (_, i) => + + )} + ; + +export default LoadingRow; diff --git a/client/components/GenericTable/SortIcon.tsx b/client/components/GenericTable/SortIcon.tsx new file mode 100644 index 00000000000..d629171d5c5 --- /dev/null +++ b/client/components/GenericTable/SortIcon.tsx @@ -0,0 +1,14 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +type SortIconProps = { + direction?: 'asc' | 'desc'; +}; + +const SortIcon: FC = ({ direction }) => + + + + ; + +export default SortIcon; diff --git a/client/components/GenericTable.js b/client/components/GenericTable/index.js similarity index 53% rename from client/components/GenericTable.js rename to client/components/GenericTable/index.js index efd134ed07b..2bdc58b6e0f 100644 --- a/client/components/GenericTable.js +++ b/client/components/GenericTable/index.js @@ -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 - - - ; -} - -export function Th({ children, active, direction, sort, onClick, align, ...props }) { - const fn = useMemo(() => () => onClick && onClick(sort), [sort, onClick]); - return - {children}{sort && } - ; -} - -const LoadingRow = ({ cols }) => - - - - - - - - - - - - { Array.from({ length: cols - 1 }, (_, i) => - - )} -; - -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 <> - + {typeof renderFilter === 'function' ? renderFilter({ onChange: setFilter, ...props }) : null} {results && !results.length ? {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, }); diff --git a/client/contexts/ConnectionStatusContext.ts b/client/contexts/ConnectionStatusContext.ts index aca0ec9e367..ac36bfce9dd 100644 --- a/client/contexts/ConnectionStatusContext.ts +++ b/client/contexts/ConnectionStatusContext.ts @@ -1,17 +1,15 @@ import { createContext, useContext } from 'react'; -type ConnectionStatusContextValue = { +export type ConnectionStatusContextValue = { connected: boolean; - retryCount: number; - retryTime: number; + retryCount?: number; + retryTime?: number; status: 'connected' | 'connecting' | 'failed' | 'waiting' | 'offline'; reconnect: () => void; }; export const ConnectionStatusContext = createContext({ connected: true, - retryCount: 0, - retryTime: 0, status: 'connected', reconnect: () => undefined, }); diff --git a/client/contexts/CustomSoundContext.ts b/client/contexts/CustomSoundContext.ts index 9e74363ed5a..b82b27d90de 100644 --- a/client/contexts/CustomSoundContext.ts +++ b/client/contexts/CustomSoundContext.ts @@ -1,7 +1,7 @@ import { createContext, useContext } from 'react'; type CustomSoundContextValue = { - play: () => void; + play: (sound: string, options?: { volume?: number; loop?: boolean }) => void; }; export const CustomSoundContext = createContext({ diff --git a/client/contexts/TranslationContext.js b/client/contexts/TranslationContext.js deleted file mode 100644 index c97daaea138..00000000000 --- a/client/contexts/TranslationContext.js +++ /dev/null @@ -1,20 +0,0 @@ -import { createContext, useContext } from 'react'; - -const translate = (key) => key; -translate.has = () => true; - -export const TranslationContext = createContext({ - languages: [{ - name: 'Default', - en: 'Default', - key: '', - }], - language: '', - loadLanguage: async () => {}, - translate, -}); - -export const useLanguages = () => useContext(TranslationContext).languages; -export const useLanguage = () => useContext(TranslationContext).language; -export const useLoadLanguage = () => useContext(TranslationContext).loadLanguage; -export const useTranslation = () => useContext(TranslationContext).translate; diff --git a/client/contexts/TranslationContext.ts b/client/contexts/TranslationContext.ts new file mode 100644 index 00000000000..15bd4c64bbf --- /dev/null +++ b/client/contexts/TranslationContext.ts @@ -0,0 +1,45 @@ +import { createContext, useContext } from 'react'; + +export type TranslationLanguage = { + name: string; + en: string; + key: string; +}; + +export type TranslationContextValue = { + languages: TranslationLanguage[]; + language: TranslationLanguage['key']; + loadLanguage: (language: TranslationLanguage['key']) => Promise; + translate: { + (key: string, ...replaces: unknown[]): string; + has: (key: string) => boolean; + }; +}; + +export const TranslationContext = createContext({ + languages: [{ + name: 'Default', + en: 'Default', + key: '', + }], + language: '', + loadLanguage: async () => undefined, + translate: Object.assign( + (key: string) => key, + { + has: () => true, + }, + ), +}); + +export const useLanguages = (): TranslationContextValue['languages'] => + useContext(TranslationContext).languages; + +export const useLanguage = (): TranslationContextValue['language'] => + useContext(TranslationContext).language; + +export const useLoadLanguage = (): TranslationContextValue['loadLanguage'] => + useContext(TranslationContext).loadLanguage; + +export const useTranslation = (): TranslationContextValue['translate'] => + useContext(TranslationContext).translate; diff --git a/client/omnichannel/agents/AgentsPage.js b/client/omnichannel/agents/AgentsPage.js index 20121bad854..7ccd7adad1a 100644 --- a/client/omnichannel/agents/AgentsPage.js +++ b/client/omnichannel/agents/AgentsPage.js @@ -1,28 +1,13 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon } from '@rocket.chat/fuselage'; +import React, { useState } from 'react'; +import { Button, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { UserAutoComplete } from '../../components/basic/AutoComplete'; import Page from '../../components/basic/Page'; +import FilterByText from '../../components/FilterByText'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointAction } from '../../hooks/useEndpointAction'; -import { GenericTable } from '../../components/GenericTable'; -import { UserAutoComplete } from '../../components/basic/AutoComplete'; - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; - function AddAgent({ reload, ...props }) { const t = useTranslation(); @@ -62,7 +47,15 @@ function AgentsPage({ - + } + /> {children} diff --git a/client/omnichannel/agents/AgentsRoute.js b/client/omnichannel/agents/AgentsRoute.js index 6192d225ff0..a6317671133 100644 --- a/client/omnichannel/agents/AgentsRoute.js +++ b/client/omnichannel/agents/AgentsRoute.js @@ -3,7 +3,7 @@ import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.ch import React, { useMemo, useCallback, useState } from 'react'; import { Box, Table, Icon, Button } from '@rocket.chat/fuselage'; -import { Th } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; import { useEndpointAction } from '../../hooks/useEndpointAction'; @@ -46,7 +46,10 @@ export function RemoveAgentButton({ _id, reload }) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); return @@ -84,7 +87,10 @@ export function AgentInfoActions({ reload }) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); const handleEditClick = useMutableCallback(() => agentsRoute.push({ @@ -143,11 +149,11 @@ function AgentsRoute() { const header = useMemo(() => [ - {t('Name')}, - mediaQuery && {t('Username')}, - {t('Email')}, - {t('Livechat_status')}, - {t('Remove')}, + {t('Name')}, + mediaQuery && {t('Username')}, + {t('Email')}, + {t('Livechat_status')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]); const renderRow = useCallback(({ emails, _id, username, name, avatarETag, statusLivechat }) => diff --git a/client/omnichannel/currentChats/CurrentChatsPage.js b/client/omnichannel/currentChats/CurrentChatsPage.js index c9a3e887141..39817980f1c 100644 --- a/client/omnichannel/currentChats/CurrentChatsPage.js +++ b/client/omnichannel/currentChats/CurrentChatsPage.js @@ -9,7 +9,7 @@ import Page from '../../components/basic/Page'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; import { usePermission } from '../../contexts/AuthorizationContext'; -import { GenericTable } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useMethod } from '../../contexts/ServerContext'; import DeleteWarningModal from '../../components/DeleteWarningModal'; import { useSetModal } from '../../contexts/ModalContext'; @@ -126,7 +126,10 @@ const FilterByText = ({ setFilter, reload, ...props }) => { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); @@ -189,7 +192,16 @@ function CurrentChatsPage({ - + } + /> {children} diff --git a/client/omnichannel/currentChats/CurrentChatsRoute.js b/client/omnichannel/currentChats/CurrentChatsRoute.js index c204c27cf90..750635d5aad 100644 --- a/client/omnichannel/currentChats/CurrentChatsRoute.js +++ b/client/omnichannel/currentChats/CurrentChatsRoute.js @@ -7,7 +7,7 @@ import moment from 'moment'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Th } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; import { useMethod } from '../../contexts/ServerContext'; @@ -45,7 +45,10 @@ export function RemoveChatButton({ _id, reload }) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); return @@ -120,13 +123,13 @@ function CurrentChatsRoute() { const { data, reload } = useEndpointDataExperimental('livechat/rooms', query) || {}; const header = useMemo(() => [ - {t('Name')}, - {t('Department')}, - {t('Served_By')}, - {t('Started_At')}, - {t('Last_Message')}, - {t('Status')}, - {t('Remove')}, + {t('Name')}, + {t('Department')}, + {t('Served_By')}, + {t('Started_At')}, + {t('Last_Message')}, + {t('Status')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t]); const renderRow = useCallback(({ _id, fname, servedBy, ts, lm, department, open }) => onRowClick(_id)} action qa-user-id={_id}> diff --git a/client/omnichannel/customFields/CustomFieldsTable.js b/client/omnichannel/customFields/CustomFieldsTable.js index 733772c5af3..8a21df18a2e 100644 --- a/client/omnichannel/customFields/CustomFieldsTable.js +++ b/client/omnichannel/customFields/CustomFieldsTable.js @@ -40,7 +40,10 @@ export function RemoveCustomFieldButton({ _id, reload }) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); return diff --git a/client/omnichannel/departments/DepartmentsAgentsTable.js b/client/omnichannel/departments/DepartmentsAgentsTable.js index 8f62ac635ba..425fc5f7b06 100644 --- a/client/omnichannel/departments/DepartmentsAgentsTable.js +++ b/client/omnichannel/departments/DepartmentsAgentsTable.js @@ -3,7 +3,7 @@ import { useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState, useEffect } from 'react'; import { Box, Table, Icon, Button, NumberInput } from '@rocket.chat/fuselage'; -import { Th, GenericTable } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointAction } from '../../hooks/useEndpointAction'; import UserAvatar from '../../components/basic/avatar/UserAvatar'; @@ -53,7 +53,10 @@ export function RemoveAgentButton({ agentId, setAgentList, agentList }) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); return ; @@ -130,10 +133,10 @@ function DepartmentsAgentsTable({ agents, setAgentListFinal }) { - {t('Name')} - {t('Count')} - {t('Order')} - {t('Remove')} + {t('Name')} + {t('Count')} + {t('Order')} + {t('Remove')} } results={agentList} total={agentList?.length} diff --git a/client/omnichannel/departments/DepartmentsPage.js b/client/omnichannel/departments/DepartmentsPage.js index a3995b05ed8..0c0efca7288 100644 --- a/client/omnichannel/departments/DepartmentsPage.js +++ b/client/omnichannel/departments/DepartmentsPage.js @@ -1,28 +1,12 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon } from '@rocket.chat/fuselage'; +import React from 'react'; +import { Button, Icon } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import Page from '../../components/basic/Page'; -import { useTranslation } from '../../contexts/TranslationContext'; -import { GenericTable } from '../../components/GenericTable'; +import FilterByText from '../../components/FilterByText'; +import GenericTable from '../../components/GenericTable'; import { useRoute } from '../../contexts/RouterContext'; - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; - function DepartmentsPage({ data, header, @@ -45,7 +29,15 @@ function DepartmentsPage({ - + } + /> {children} diff --git a/client/omnichannel/departments/DepartmentsRoute.js b/client/omnichannel/departments/DepartmentsRoute.js index 1a6dd222b54..46c08269193 100644 --- a/client/omnichannel/departments/DepartmentsRoute.js +++ b/client/omnichannel/departments/DepartmentsRoute.js @@ -3,7 +3,7 @@ import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hoo import React, { useMemo, useCallback, useState } from 'react'; import { Table, Icon } from '@rocket.chat/fuselage'; -import { Th } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; import { useEndpointAction } from '../../hooks/useEndpointAction'; @@ -70,12 +70,12 @@ function DepartmentsRoute() { const { data, reload } = useEndpointDataExperimental('livechat/department', query) || {}; const header = useMemo(() => [ - {t('Name')}, - {t('Description')}, - {t('Num_Agents')}, - {t('Enabled')}, - {t('Show_on_registration_page')}, - {t('Remove')}, + {t('Name')}, + {t('Description')}, + {t('Num_Agents')}, + {t('Enabled')}, + {t('Show_on_registration_page')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t]); const renderRow = useCallback(({ name, _id, description, numAgents, enabled, showOnRegistration }) => diff --git a/client/omnichannel/managers/ManagersPage.js b/client/omnichannel/managers/ManagersPage.js index e925b110cde..57585e8fa1c 100644 --- a/client/omnichannel/managers/ManagersPage.js +++ b/client/omnichannel/managers/ManagersPage.js @@ -1,27 +1,13 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon } from '@rocket.chat/fuselage'; +import React, { useState } from 'react'; +import { Button, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { UserAutoComplete } from '../../components/basic/AutoComplete'; import Page from '../../components/basic/Page'; +import FilterByText from '../../components/FilterByText'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointAction } from '../../hooks/useEndpointAction'; -import { GenericTable } from '../../components/GenericTable'; -import { UserAutoComplete } from '../../components/basic/AutoComplete'; - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; function AddManager({ reload, ...props }) { const t = useTranslation(); @@ -61,7 +47,15 @@ function ManagersPage({ - + } + /> {children} diff --git a/client/omnichannel/managers/ManagersRoute.js b/client/omnichannel/managers/ManagersRoute.js index 273eb2cacc9..febd8bfb6fd 100644 --- a/client/omnichannel/managers/ManagersRoute.js +++ b/client/omnichannel/managers/ManagersRoute.js @@ -3,7 +3,7 @@ import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.ch import React, { useMemo, useCallback, useState } from 'react'; import { Box, Table, Icon, Button } from '@rocket.chat/fuselage'; -import { Th } from '../../components/GenericTable'; +import GenericTable from '../../components/GenericTable'; import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; import { useEndpointAction } from '../../hooks/useEndpointAction'; @@ -68,10 +68,10 @@ export function ManagersRoute() { const header = useMemo(() => [ - {t('Name')}, - mediaQuery && {t('Username')}, - {t('Email')}, - {t('Remove')}, + {t('Name')}, + mediaQuery && {t('Username')}, + {t('Email')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]); const renderRow = useCallback(({ emails, _id, username, name, avatarETag }) => diff --git a/client/omnichannel/triggers/TriggersTable.js b/client/omnichannel/triggers/TriggersTable.js index b10534e1c69..f57ce3a5f83 100644 --- a/client/omnichannel/triggers/TriggersTable.js +++ b/client/omnichannel/triggers/TriggersTable.js @@ -58,7 +58,10 @@ const TriggersRow = memo(function TriggersRow(props) { setModal(); }; - setModal( setModal()}/>); + setModal( setModal()} + />); }); return { const avatarBase = useMemo(() => ({ baseUrl: '' }), []); return ; -} +}; + +export default AvatarUrlProvider; diff --git a/client/providers/ConnectionStatusProvider.js b/client/providers/ConnectionStatusProvider.js deleted file mode 100644 index ed820da5cdf..00000000000 --- a/client/providers/ConnectionStatusProvider.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import React from 'react'; - -import { ConnectionStatusContext } from '../contexts/ConnectionStatusContext'; -import { useReactiveValue } from '../hooks/useReactiveValue'; - -const getStatus = () => ({ - ...Meteor.status(), - reconnect: Meteor.reconnect, -}); - -export function ConnectionStatusProvider({ children }) { - const status = useReactiveValue(getStatus); - - return ; -} diff --git a/client/providers/ConnectionStatusProvider.tsx b/client/providers/ConnectionStatusProvider.tsx new file mode 100644 index 00000000000..2763ce8eac4 --- /dev/null +++ b/client/providers/ConnectionStatusProvider.tsx @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; +import React, { FC } from 'react'; + +import { ConnectionStatusContext, ConnectionStatusContextValue } from '../contexts/ConnectionStatusContext'; +import { useReactiveValue } from '../hooks/useReactiveValue'; + +const getValue = (): ConnectionStatusContextValue => ({ + ...Meteor.status(), + reconnect: Meteor.reconnect, +}); + +const ConnectionStatusProvider: FC = ({ children }) => { + const status = useReactiveValue(getValue); + + return ; +}; + +export default ConnectionStatusProvider; diff --git a/client/providers/CustomSoundProvider.tsx b/client/providers/CustomSoundProvider.tsx new file mode 100644 index 00000000000..cec0ae32a92 --- /dev/null +++ b/client/providers/CustomSoundProvider.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; + +import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; +import { CustomSoundContext } from '../contexts/CustomSoundContext'; + +const CustomSoundProvider: FC = ({ children }) => + ; + +export default CustomSoundProvider; diff --git a/client/providers/CustomSoundProvides.js b/client/providers/CustomSoundProvides.js deleted file mode 100644 index d5a41bc54fa..00000000000 --- a/client/providers/CustomSoundProvides.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; -import { CustomSoundContext } from '../contexts/CustomSoundContext'; - - -export function CustomSoundProvider({ children }) { - return ; -} diff --git a/client/providers/MeteorProvider.js b/client/providers/MeteorProvider.js index 22086780934..475f6cb083f 100644 --- a/client/providers/MeteorProvider.js +++ b/client/providers/MeteorProvider.js @@ -1,20 +1,20 @@ import React from 'react'; import AuthorizationProvider from './AuthorizationProvider'; -import { ConnectionStatusProvider } from './ConnectionStatusProvider'; -import { RouterProvider } from './RouterProvider'; +import AvatarUrlProvider from './AvatarUrlProvider'; +import ConnectionStatusProvider from './ConnectionStatusProvider'; +import CustomSoundProvider from './CustomSoundProvider'; +import ModalProvider from './ModalProvider'; +import RouterProvider from './RouterProvider'; +import ServerProvider from './ServerProvider'; import SessionProvider from './SessionProvider'; import SettingsProvider from './SettingsProvider'; -import { ServerProvider } from './ServerProvider'; -import { SidebarProvider } from './SidebarProvider'; -import { TranslationProvider } from './TranslationProvider'; -import { ToastMessagesProvider } from './ToastMessagesProvider'; +import SidebarProvider from './SidebarProvider'; +import ToastMessagesProvider from './ToastMessagesProvider'; +import TranslationProvider from './TranslationProvider'; import UserProvider from './UserProvider'; -import { AvatarUrlProvider } from './AvatarUrlProvider'; -import { CustomSoundProvider } from './CustomSoundProvides'; -import ModalProvider from './ModalProvider'; -export function MeteorProvider({ children }) { +function MeteorProvider({ children }) { return diff --git a/client/providers/RouterProvider.tsx b/client/providers/RouterProvider.tsx index 15cc53504ac..f059dcc0f3f 100644 --- a/client/providers/RouterProvider.tsx +++ b/client/providers/RouterProvider.tsx @@ -85,5 +85,7 @@ const contextValue = { queryCurrentRoute, }; -export const RouterProvider: FC = ({ children }) => +const RouterProvider: FC = ({ children }) => ; + +export default RouterProvider; diff --git a/client/providers/ServerProvider.js b/client/providers/ServerProvider.js index d847176fcf8..c378261972e 100644 --- a/client/providers/ServerProvider.js +++ b/client/providers/ServerProvider.js @@ -53,6 +53,7 @@ const contextValue = { getStream, }; -export function ServerProvider({ children }) { - return ; -} +const ServerProvider = ({ children }) => + ; + +export default ServerProvider; diff --git a/client/providers/SidebarProvider.tsx b/client/providers/SidebarProvider.tsx index 1731763fe98..76363fa36a1 100644 --- a/client/providers/SidebarProvider.tsx +++ b/client/providers/SidebarProvider.tsx @@ -14,8 +14,10 @@ const setOpen = (open: boolean | ((isOpen: boolean) => boolean)): void => { return open ? menu.open() : menu.close(); }; -export const SidebarProvider: FC = ({ children }) => +const SidebarProvider: FC = ({ children }) => (getOpen), setOpen]} />; + +export default SidebarProvider; diff --git a/client/providers/ToastMessagesProvider.tsx b/client/providers/ToastMessagesProvider.tsx index 3e197dd71bf..c5adfeaf276 100644 --- a/client/providers/ToastMessagesProvider.tsx +++ b/client/providers/ToastMessagesProvider.tsx @@ -21,5 +21,7 @@ const contextValue = { dispatch, }; -export const ToastMessagesProvider: FC = ({ children }) => +const ToastMessagesProvider: FC = ({ children }) => ; + +export default ToastMessagesProvider; diff --git a/client/providers/TranslationProvider.js b/client/providers/TranslationProvider.js index ae8e1f3ff3c..1cd0e9cf625 100644 --- a/client/providers/TranslationProvider.js +++ b/client/providers/TranslationProvider.js @@ -51,19 +51,23 @@ const getLanguage = () => TAPi18n.getLanguage(); const loadLanguage = (language) => TAPi18n._loadLanguage(language); -export function TranslationProvider({ children }) { +function TranslationProvider({ children }) { const languages = useReactiveValue(getLanguages); const language = useReactiveValue(getLanguage); + const translate = useMemo(() => createTranslateFunction(language), [language]); + const value = useMemo(() => ({ languages, language, loadLanguage, translate, - }), - [languages, language, loadLanguage, translate]); + }), [languages, language, translate]); + return ; } + +export default TranslationProvider; diff --git a/client/types/fuselage.d.ts b/client/types/fuselage.d.ts index d8891f9bd85..dc98a120a0f 100644 --- a/client/types/fuselage.d.ts +++ b/client/types/fuselage.d.ts @@ -1,12 +1,17 @@ declare module '@rocket.chat/fuselage' { import { css } from '@rocket.chat/css-in-js'; import { - CSSProperties, AllHTMLAttributes, + Context, + CSSProperties, + Dispatch, ElementType, ForwardRefExoticComponent, PropsWithChildren, + ReactNode, RefAttributes, + SetStateAction, + SVGAttributes, } from 'react'; type CssClassName = ReturnType; @@ -141,7 +146,10 @@ declare module '@rocket.chat/fuselage' { minSize?: CSSProperties['blockSize']; maxSize?: CSSProperties['blockSize']; fontScale?: FontScale; - }> & Omit, 'className'> & RefAttributes; + }> + & Omit, 'className'> + & Omit, keyof AllHTMLAttributes> + & RefAttributes; export const Box: ForwardRefExoticComponent; @@ -154,10 +162,19 @@ declare module '@rocket.chat/fuselage' { Item: ForwardRefExoticComponent; }; + type AvatarProps = BoxProps; + export const Avatar: ForwardRefExoticComponent & { + Context: Context<{ + baseUrl: string; + }>; + }; + type ButtonProps = BoxProps & { primary?: boolean; ghost?: boolean; danger?: boolean; + small?: boolean; + square?: boolean; }; export const Button: ForwardRefExoticComponent; @@ -172,6 +189,11 @@ declare module '@rocket.chat/fuselage' { type CalloutProps = BoxProps; export const Callout: ForwardRefExoticComponent; + type CheckBoxProps = BoxProps & { + indeterminate?: boolean; + }; + export const CheckBox: ForwardRefExoticComponent; + type ChevronProps = Omit & { size?: BoxProps['width']; right?: boolean; @@ -181,6 +203,9 @@ declare module '@rocket.chat/fuselage' { }; export const Chevron: ForwardRefExoticComponent; + type ChipProps = BoxProps; + export const Chip: ForwardRefExoticComponent; + type FieldProps = BoxProps; export const Field: ForwardRefExoticComponent & { Row: ForwardRefExoticComponent; @@ -198,14 +223,86 @@ declare module '@rocket.chat/fuselage' { export const Icon: ForwardRefExoticComponent; type InputBoxProps = BoxProps; - export const InputBox: ForwardRefExoticComponent; + type InputBoxSkeletonProps = BoxProps; + export const InputBox: ForwardRefExoticComponent & { + Skeleton: ForwardRefExoticComponent; + }; + + type ModalProps = BoxProps; + type ModalHeaderProps = BoxProps; + type ModalTitleProps = BoxProps; + type ModalCloseProps = BoxProps; + type ModalContentProps = BoxProps; + type ModalFooterProps = BoxProps; + export const Modal: ForwardRefExoticComponent & { + Header: ForwardRefExoticComponent; + Title: ForwardRefExoticComponent; + Close: ForwardRefExoticComponent; + Content: ForwardRefExoticComponent; + Footer: ForwardRefExoticComponent; + }; type NumberInputProps = BoxProps; export const NumberInput: ForwardRefExoticComponent; + type PaginationProps = BoxProps & { + count: number; + current?: number; + itemsPerPage?: 25 | 50 | 100; + itemsPerPageLabel?: () => string; + showingResultsLabel?: (props: { count: number; current: number; itemsPerPage: 25 | 50 | 100 }) => string; + onSetCurrent?: Dispatch>; + onSetItemsPerPage?: Dispatch>; + }; + export const Pagination: ForwardRefExoticComponent; + + type PasswordInputProps = BoxProps & { + error?: string; + }; + export const PasswordInput: ForwardRefExoticComponent; + + type SearchInputProps = BoxProps & { + addon?: ReactNode; + error?: string; + }; + export const SearchInput: ForwardRefExoticComponent; + + type SkeletonProps = BoxProps & { + variant?: 'rect'; + }; + export const Skeleton: ForwardRefExoticComponent; + + type TableProps = BoxProps; + type TableHeadProps = BoxProps; + type TableBodyProps = BoxProps; + type TableRowProps = Omit & { + action?: boolean; + }; + type TableCellProps = BoxProps & { + align?: 'start' | 'center' | 'end'; + clickable?: boolean; + }; + export const Table: ForwardRefExoticComponent & { + Head: ForwardRefExoticComponent; + Body: ForwardRefExoticComponent; + Row: ForwardRefExoticComponent; + Cell: ForwardRefExoticComponent; + }; + + type TagProps = BoxProps & { + variant?: 'secondary' | 'primary' | 'danger'; + }; + export const Tag: ForwardRefExoticComponent; + type TextAreaInputProps = BoxProps; export const TextAreaInput: ForwardRefExoticComponent; + type TextInputProps = BoxProps & { + addon?: ReactNode; + error?: string; + }; + export const TextInput: ForwardRefExoticComponent; + type TileProps = BoxProps; export const Tile: ForwardRefExoticComponent; @@ -218,11 +315,6 @@ declare module '@rocket.chat/fuselage' { type ToggleSwitchProps = BoxProps; export const ToggleSwitch: ForwardRefExoticComponent; - type TextInputProps = BoxProps & { - error?: string; - }; - export const TextInput: ForwardRefExoticComponent; - type MarginsProps = PropsWithChildren<{ all?: BoxProps['margin']; block?: BoxProps['marginBlock']; diff --git a/client/views/directory/ChannelsTab.js b/client/views/directory/ChannelsTab.js index 3fbc962b326..4a9436e4c25 100644 --- a/client/views/directory/ChannelsTab.js +++ b/client/views/directory/ChannelsTab.js @@ -1,13 +1,14 @@ -import { Box, Margins, Table, Avatar, Tag, Icon, TextInput } from '@rocket.chat/fuselage'; +import { Box, Margins, Table, Avatar, Tag, Icon } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo, useState, useCallback, useEffect } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; -import { GenericTable, Th } from '../../components/GenericTable'; import MarkdownText from '../../components/basic/MarkdownText'; +import FilterByText from '../../components/FilterByText'; +import GenericTable from '../../components/GenericTable'; import NotAuthorizedPage from '../../components/NotAuthorizedPage'; -import { useTranslation } from '../../contexts/TranslationContext'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useRoute } from '../../contexts/RouterContext'; +import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointData } from '../../hooks/useEndpointData'; import { useFormatDate } from '../../hooks/useFormatDate'; import { roomTypes } from '../../../app/utils/client'; @@ -25,20 +26,6 @@ function RoomTags({ room }) { ; } -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 - } onChange={handleChange} value={text} /> - ; -}; - function ChannelsTable() { const t = useTranslation(); const [sort, setSort] = useState(['name', 'asc']); @@ -59,10 +46,10 @@ function ChannelsTable() { }, [sort]); const header = useMemo(() => [ - {t('Name')}, - {t('Users')}, - mediaQuery && {t('Created_at')}, - mediaQuery && {t('Last_Message')}, + {t('Name')}, + {t('Users')}, + mediaQuery && {t('Created_at')}, + mediaQuery && {t('Last_Message')}, ].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]); const channelRoute = useRoute('channel'); @@ -105,7 +92,14 @@ function ChannelsTable() { } , [formatDate, mediaQuery, onClick]); - return ; + return } + renderRow={renderRow} + results={data.result} + setParams={setParams} + total={data.total} + />; } export default function ChannelsTab(props) { diff --git a/client/views/directory/UserTab.js b/client/views/directory/UserTab.js index 222c11d040a..a948f4e727f 100644 --- a/client/views/directory/UserTab.js +++ b/client/views/directory/UserTab.js @@ -1,31 +1,18 @@ -import React, { useMemo, useState, useCallback, useEffect } from 'react'; -import { Box, Table, Flex, TextInput, Icon } from '@rocket.chat/fuselage'; +import React, { useMemo, useState, useCallback } from 'react'; +import { Box, Table, Flex } from '@rocket.chat/fuselage'; import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import { GenericTable, Th } from '../../components/GenericTable'; -import { useTranslation } from '../../contexts/TranslationContext'; -import { useRoute } from '../../contexts/RouterContext'; +import UserAvatar from '../../components/basic/avatar/UserAvatar'; +import MarkdownText from '../../components/basic/MarkdownText'; +import FilterByText from '../../components/FilterByText'; +import GenericTable from '../../components/GenericTable'; +import NotAuthorizedPage from '../../components/NotAuthorizedPage'; import { usePermission } from '../../contexts/AuthorizationContext'; -import { useQuery } from './hooks'; +import { useRoute } from '../../contexts/RouterContext'; +import { useTranslation } from '../../contexts/TranslationContext'; import { useEndpointData } from '../../hooks/useEndpointData'; import { useFormatDate } from '../../hooks/useFormatDate'; -import UserAvatar from '../../components/basic/avatar/UserAvatar'; -import NotAuthorizedPage from '../../components/NotAuthorizedPage'; -import MarkdownText from '../../components/basic/MarkdownText'; - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - - useEffect(() => { - setFilter({ text }); - }, [text, setFilter]); - - return - } onChange={handleChange} value={text} /> - ; -}; +import { useQuery } from './hooks'; function UserTable({ workspace = 'local', @@ -52,10 +39,10 @@ function UserTable({ }, [sort]); const header = useMemo(() => [ - {t('Name')}, - mediaQuery && canViewFullOtherUserInfo && {t('Email')}, - federation && {t('Domain')}, - mediaQuery && {t('Joined_at')}, + {t('Name')}, + mediaQuery && canViewFullOtherUserInfo && {t('Email')}, + federation && {t('Domain')}, + mediaQuery && {t('Joined_at')}, ].filter(Boolean), [sort, onHeaderClick, t, mediaQuery, canViewFullOtherUserInfo, federation]); const directRoute = useRoute('direct'); @@ -100,7 +87,14 @@ function UserTable({ } , [mediaQuery, federation, canViewFullOtherUserInfo, formatDate, onClick]); - return ; + return } + renderRow={renderRow} + results={data.result} + setParams={setParams} + total={data.total} + />; } export default function UserTab(props) { diff --git a/ee/client/audit/AuditLogTable.js b/ee/client/audit/AuditLogTable.js index e1b06459b53..7e64bbd11d4 100644 --- a/ee/client/audit/AuditLogTable.js +++ b/ee/client/audit/AuditLogTable.js @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { Box, Table } from '@rocket.chat/fuselage'; import UserAvatar from '../../../client/components/basic/avatar/UserAvatar'; -import { GenericTable } from '../../../client/components/GenericTable'; +import GenericTable from '../../../client/components/GenericTable'; import { useTranslation } from '../../../client/contexts/TranslationContext'; import { useFormatDateAndTime } from '../../../client/hooks/useFormatDateAndTime'; import { useFormatDate } from '../../../client/hooks/useFormatDate'; diff --git a/ee/client/omnichannel/BusinessHoursTable.js b/ee/client/omnichannel/BusinessHoursTable.js index 05ddaee7095..1711efdec47 100644 --- a/ee/client/omnichannel/BusinessHoursTable.js +++ b/ee/client/omnichannel/BusinessHoursTable.js @@ -1,17 +1,17 @@ -import { Table, Callout, Box, TextInput, Icon, Button } from '@rocket.chat/fuselage'; +import { Table, Callout, Icon, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { useState, memo, useMemo, useEffect } from 'react'; +import React, { memo, useMemo, useState } from 'react'; +import DeleteWarningModal from '../../../client/components/DeleteWarningModal'; import GenericTable from '../../../client/components/GenericTable'; +import FilterByText from '../../../client/components/FilterByText'; +import { useSetModal } from '../../../client/contexts/ModalContext'; import { useRoute } from '../../../client/contexts/RouterContext'; +import { useMethod } from '../../../client/contexts/ServerContext'; +import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext'; import { useTranslation } from '../../../client/contexts/TranslationContext'; import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint'; import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../client/hooks/useEndpointDataExperimental'; -import DeleteWarningModal from '../../../client/components/DeleteWarningModal'; -import { useSetModal } from '../../../client/contexts/ModalContext'; -import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext'; -import { useMethod } from '../../../client/contexts/ServerContext'; - export function RemoveBusinessHourButton({ _id, type, reload }) { const removeBusinessHour = useMethod('livechat:removeBusinessHour'); @@ -50,22 +50,6 @@ export function RemoveBusinessHourButton({ _id, type, reload }) { ; } -const FilterByText = memo(({ setFilter, ...props }) => { - const t = useTranslation(); - - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value), []); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - - return e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}); - const BusinessHoursRow = memo(function BusinessHoursRow(props) { const { _id, @@ -183,7 +167,7 @@ export function BusinessHoursTable({ businessHours, totalbusinessHours, params, total={totalbusinessHours} params={params} setParams={onChangeParams} - FilterComponent={FilterByText} + renderFilter={({ onChange, ...props }) => } > {(props) => } ; diff --git a/ee/client/omnichannel/MonitorsTable.js b/ee/client/omnichannel/MonitorsTable.js index 12b6a9b5cc6..7b7a6c4e0db 100644 --- a/ee/client/omnichannel/MonitorsTable.js +++ b/ee/client/omnichannel/MonitorsTable.js @@ -1,31 +1,16 @@ -import { Table, Box, TextInput, Icon, Button } from '@rocket.chat/fuselage'; +import { Table, Icon, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { useState, memo, useEffect } from 'react'; +import React, { memo } from 'react'; -import GenericTable from '../../../client/components/GenericTable'; import DeleteWarningModal from '../../../client/components/DeleteWarningModal'; +import FilterByText from '../../../client/components/FilterByText'; +import GenericTable from '../../../client/components/GenericTable'; import { useSetModal } from '../../../client/contexts/ModalContext'; import { useMethod } from '../../../client/contexts/ServerContext'; import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext'; import { useTranslation } from '../../../client/contexts/TranslationContext'; import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint'; -const FilterByText = memo(({ setFilter, ...props }) => { - const t = useTranslation(); - - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value), []); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - - return e.preventDefault(), [])} display='flex' flexDirection='column' {...props}> - } onChange={handleChange} value={text} /> - ; -}); - const MonitorsRow = memo(function MonitorsRow(props) { const { _id, @@ -110,7 +95,7 @@ export function MonitorsTable({ monitors, totalMonitors, params, sort, onHeaderC total={totalMonitors} params={params} setParams={onChangeParams} - FilterComponent={FilterByText} + renderFilter={({ onChange, ...props }) => } > {(props) => } ; diff --git a/ee/client/omnichannel/priorities/PrioritiesPage.js b/ee/client/omnichannel/priorities/PrioritiesPage.js index 5eac71f7d06..7f53c54a1fb 100644 --- a/ee/client/omnichannel/priorities/PrioritiesPage.js +++ b/ee/client/omnichannel/priorities/PrioritiesPage.js @@ -1,28 +1,12 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon, ButtonGroup } from '@rocket.chat/fuselage'; +import React from 'react'; +import { Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import Page from '../../../../client/components/basic/Page'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; -import { GenericTable } from '../../../../client/components/GenericTable'; +import GenericTable from '../../../../client/components/GenericTable'; import { useRoute } from '../../../../client/contexts/RouterContext'; - - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; - +import FilterByText from '../../../../client/components/FilterByText'; function PrioritiesPage({ data, @@ -49,7 +33,15 @@ function PrioritiesPage({ - + } + /> {children} diff --git a/ee/client/omnichannel/priorities/PrioritiesRoute.js b/ee/client/omnichannel/priorities/PrioritiesRoute.js index 1b7507c424e..becb595ddea 100644 --- a/ee/client/omnichannel/priorities/PrioritiesRoute.js +++ b/ee/client/omnichannel/priorities/PrioritiesRoute.js @@ -4,7 +4,7 @@ import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hoo import React, { useMemo, useCallback, useState } from 'react'; import { Table, Icon, Button } from '@rocket.chat/fuselage'; -import { Th } from '../../../../client/components/GenericTable'; +import GenericTable from '../../../../client/components/GenericTable'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../../../client/hooks/useEndpointDataExperimental'; import { useMethod } from '../../../../client/contexts/ServerContext'; @@ -98,10 +98,10 @@ function PrioritiesRoute() { const { data, reload } = useEndpointDataExperimental('livechat/priorities.list', query) || {}; const header = useMemo(() => [ - {t('Name')}, - {t('Description')}, - {t('Estimated_due_time')}, - {t('Remove')}, + {t('Name')}, + {t('Description')}, + {t('Estimated_due_time')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t]); const renderRow = useCallback(({ _id, name, description, dueTimeInMinutes }) => diff --git a/ee/client/omnichannel/tags/TagsPage.js b/ee/client/omnichannel/tags/TagsPage.js index 95f692aa157..eacaa519357 100644 --- a/ee/client/omnichannel/tags/TagsPage.js +++ b/ee/client/omnichannel/tags/TagsPage.js @@ -1,29 +1,13 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon, ButtonGroup } from '@rocket.chat/fuselage'; +import React from 'react'; +import { Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import Page from '../../../../client/components/basic/Page'; +import FilterByText from '../../../../client/components/FilterByText'; +import GenericTable from '../../../../client/components/GenericTable'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; -import { GenericTable } from '../../../../client/components/GenericTable'; import { useRoute } from '../../../../client/contexts/RouterContext'; - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; - - function TagsPage({ data, header, @@ -49,7 +33,15 @@ function TagsPage({ - + } + /> {children} diff --git a/ee/client/omnichannel/tags/TagsRoute.js b/ee/client/omnichannel/tags/TagsRoute.js index 17d797475c1..1be7cdd46e7 100644 --- a/ee/client/omnichannel/tags/TagsRoute.js +++ b/ee/client/omnichannel/tags/TagsRoute.js @@ -4,7 +4,7 @@ import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hoo import React, { useMemo, useCallback, useState } from 'react'; import { Table, Icon, Button } from '@rocket.chat/fuselage'; -import { Th } from '../../../../client/components/GenericTable'; +import GenericTable from '../../../../client/components/GenericTable'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../../../client/hooks/useEndpointDataExperimental'; import { useMethod } from '../../../../client/contexts/ServerContext'; @@ -99,9 +99,9 @@ function TagsRoute() { const { data, reload } = useEndpointDataExperimental('livechat/tags.list', query) || {}; const header = useMemo(() => [ - {t('Name')}, - {t('Description')}, - {t('Remove')}, + {t('Name')}, + {t('Description')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t]); const renderRow = useCallback(({ _id, name, description }) => diff --git a/ee/client/omnichannel/units/UnitsPage.js b/ee/client/omnichannel/units/UnitsPage.js index 29d014d1bcf..22c3d6195f5 100644 --- a/ee/client/omnichannel/units/UnitsPage.js +++ b/ee/client/omnichannel/units/UnitsPage.js @@ -1,28 +1,12 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Button, Box, Icon, ButtonGroup } from '@rocket.chat/fuselage'; +import React from 'react'; +import { Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import Page from '../../../../client/components/basic/Page'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; -import { GenericTable } from '../../../../client/components/GenericTable'; +import GenericTable from '../../../../client/components/GenericTable'; import { useRoute } from '../../../../client/contexts/RouterContext'; - - -const FilterByText = ({ setFilter, ...props }) => { - const t = useTranslation(); - const [text, setText] = useState(''); - - const handleChange = useMutableCallback((event) => setText(event.currentTarget.value)); - const onSubmit = useMutableCallback((e) => e.preventDefault()); - - useEffect(() => { - setFilter({ text }); - }, [setFilter, text]); - return - } onChange={handleChange} value={text} /> - ; -}; - +import FilterByText from '../../../../client/components/FilterByText'; function UnitsPage({ data, @@ -49,7 +33,15 @@ function UnitsPage({ - + } + header={header} + renderRow={renderRow} + results={data && data.units} + total={data && data.total} + setParams={setParams} + params={params} + /> {children} diff --git a/ee/client/omnichannel/units/UnitsRoute.js b/ee/client/omnichannel/units/UnitsRoute.js index 6f0ffeda301..b6539f88676 100644 --- a/ee/client/omnichannel/units/UnitsRoute.js +++ b/ee/client/omnichannel/units/UnitsRoute.js @@ -4,7 +4,7 @@ import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hoo import React, { useMemo, useCallback, useState } from 'react'; import { Table, Icon, Button } from '@rocket.chat/fuselage'; -import { Th } from '../../../../client/components/GenericTable'; +import GenericTable from '../../../../client/components/GenericTable'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; import { useEndpointDataExperimental } from '../../../../client/hooks/useEndpointDataExperimental'; import { useMethod } from '../../../../client/contexts/ServerContext'; @@ -100,9 +100,9 @@ function UnitsRoute() { const { data, reload } = useEndpointDataExperimental('livechat/units.list', query) || {}; const header = useMemo(() => [ - {t('Name')}, - {t('Visibility')}, - {t('Remove')}, + {t('Name')}, + {t('Visibility')}, + {t('Remove')}, ].filter(Boolean), [sort, onHeaderClick, t]); const renderRow = useCallback(({ _id, name, visibility }) => diff --git a/package-lock.json b/package-lock.json index 8f8ef840da0..b5250204b1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5041,6 +5041,11 @@ "uuid": "^3.2.1" }, "dependencies": { + "adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==" + }, "typescript": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", @@ -8189,6 +8194,12 @@ "@types/chai": "*" } }, + "@types/clipboard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.1.tgz", + "integrity": "sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -17910,7 +17921,7 @@ }, "chownr": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true @@ -17945,7 +17956,7 @@ }, "debug": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -17976,7 +17987,7 @@ }, "fs-minipass": { "version": "1.2.5", - "resolved": "", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -18010,7 +18021,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -18042,7 +18053,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -18063,7 +18074,7 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true @@ -18104,14 +18115,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": "", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -18122,7 +18133,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": "", + "resolved": false, "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -18132,7 +18143,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -18142,7 +18153,7 @@ }, "ms": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true @@ -18156,7 +18167,7 @@ }, "needle": { "version": "2.3.0", - "resolved": "", + "resolved": false, "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -18168,7 +18179,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -18198,14 +18209,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": "", + "resolved": false, "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -18285,7 +18296,7 @@ }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true @@ -18305,7 +18316,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -18330,7 +18341,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": "", + "resolved": false, "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -18361,7 +18372,7 @@ }, "semver": { "version": "5.7.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true @@ -18421,7 +18432,7 @@ }, "tar": { "version": "4.4.8", - "resolved": "", + "resolved": false, "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -18461,7 +18472,7 @@ }, "yallist": { "version": "3.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true diff --git a/package.json b/package.json index 3c3d8d4ab63..b362b6b3c02 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@types/bcrypt": "^3.0.0", "@types/chai": "^4.2.12", "@types/chai-spies": "^1.0.1", + "@types/clipboard": "^2.0.1", "@types/meteor": "^1.4.49", "@types/mocha": "^8.0.3", "@types/mock-require": "^2.0.0",