diff --git a/apps/meteor/client/views/admin/customUserStatus/AddCustomUserStatus.tsx b/apps/meteor/client/views/admin/customUserStatus/AddCustomUserStatus.tsx deleted file mode 100644 index 19c283a09bb7..000000000000 --- a/apps/meteor/client/views/admin/customUserStatus/AddCustomUserStatus.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Button, ButtonGroup, TextInput, Field, Select, SelectOption } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { ReactElement, SyntheticEvent, useCallback, useState } from 'react'; - -import VerticalBar from '../../../components/VerticalBar'; - -type AddCustomUserStatusProps = { - goToNew: (id: string) => () => void; - close: () => void; - onChange: () => void; -}; - -function AddCustomUserStatus({ goToNew, close, onChange, ...props }: AddCustomUserStatusProps): ReactElement { - const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - - const [name, setName] = useState(''); - const [statusType, setStatusType] = useState('online'); - - const saveStatus = useMethod('insertOrUpdateUserStatus'); - const handleSave = useCallback(async () => { - try { - const result = await saveStatus({ - name, - statusType, - }); - dispatchToastMessage({ - type: 'success', - message: t('Custom_User_Status_Updated_Successfully'), - }); - goToNew(result)(); - onChange(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: String(error) }); - } - }, [dispatchToastMessage, goToNew, name, onChange, saveStatus, statusType, t]); - - const presenceOptions: SelectOption[] = [ - ['online', t('Online')], - ['busy', t('Busy')], - ['away', t('Away')], - ['offline', t('Offline')], - ]; - - return ( - - - {t('Name')} - - ): void => setName(e.currentTarget.value)} - placeholder={t('Name')} - /> - - - - {t('Presence')} - - } + /> + + {errors?.statusType && {t('error-the-field-is-required', { field: t('Presence') })}} + + + + + + + + + + {_id && ( + + + + + + + + )} + + ); +}; + +export default CustomUserStatusForm; diff --git a/apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatusWithData.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx similarity index 53% rename from apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatusWithData.tsx rename to apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx index b926de83afee..ace27c478ec8 100644 --- a/apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatusWithData.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusFormWithData.tsx @@ -1,26 +1,36 @@ -import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage'; +import { IUserStatus } from '@rocket.chat/core-typings'; +import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox, Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, FC } from 'react'; +import React, { useMemo, ReactElement } from 'react'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; -import EditCustomUserStatus from './EditCustomUserStatus'; +import CustomUserStatusForm from './CustomUserStatusForm'; -type EditCustomUserStatusWithDataProps = { - _id: string | undefined; - close: () => void; - onChange: () => void; +type CustomUserStatusFormWithDataProps = { + _id?: IUserStatus['_id']; + onClose: () => void; + onReload: () => void; }; -export const EditCustomUserStatusWithData: FC = ({ _id, onChange, ...props }) => { +const CustomUserStatusFormWithData = ({ _id, onReload, onClose }: CustomUserStatusFormWithDataProps): ReactElement => { const t = useTranslation(); const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]); const { value: data, phase: state, error, reload } = useEndpointData('custom-user-status.list', query); + const handleReload = (): void => { + onReload?.(); + reload?.(); + }; + + if (!_id) { + return ; + } + if (state === AsyncStatePhase.LOADING) { return ( - + @@ -44,18 +54,13 @@ export const EditCustomUserStatusWithData: FC if (error || !data || data.statuses.length < 1) { return ( - - {t('Custom_User_Status_Error_Invalid_User_Status')} + + {t('Custom_User_Status_Error_Invalid_User_Status')} ); } - const handleChange = (): void => { - onChange?.(); - reload?.(); - }; - - return ; + return ; }; -export default EditCustomUserStatusWithData; +export default CustomUserStatusFormWithData; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx index 7e91b75803f7..89695f9f648d 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx @@ -1,59 +1,27 @@ import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useState, useCallback, ReactNode } from 'react'; +import React, { useCallback, ReactNode, useRef } from 'react'; import Page from '../../../components/Page'; import VerticalBar from '../../../components/VerticalBar'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import { AsyncStatePhase } from '../../../lib/asyncState'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -import AddCustomUserStatus from './AddCustomUserStatus'; -import CustomUserStatus, { paramsType, SortType } from './CustomUserStatus'; -import EditCustomUserStatusWithData from './EditCustomUserStatusWithData'; +import CustomUserStatusFormWithData from './CustomUserStatusFormWithData'; +import CustomUserStatusTable from './CustomUserStatusTable'; -function CustomUserStatusRoute(): ReactNode { +const CustomUserStatusRoute = (): ReactNode => { + const t = useTranslation(); const route = useRoute('custom-user-status'); const context = useRouteParameter('context'); const id = useRouteParameter('id'); const canManageUserStatus = usePermission('manage-user-status'); - const t = useTranslation(); - - const [params, setParams] = useState(() => ({ text: '', current: 0, itemsPerPage: 25 })); - const [sort, setSort] = useState(() => ['name', 'asc']); - - const { text, itemsPerPage, current } = useDebouncedValue(params, 500); - const [column, direction] = useDebouncedValue(sort, 500); - const query = useMemo( - () => ({ - query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), - sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, itemsPerPage, current, column, direction], - ); - - const { reload, ...result } = useEndpointData('custom-user-status.list', query); - - const handleItemClick = (id: string) => (): void => { + const handleItemClick = (id: string): void => { route.push({ context: 'edit', id, }); }; - const handleHeaderClick = (id: SortType[0]): void => { - setSort(([sortBy, sortDirection]) => { - if (sortBy === id) { - return [id, sortDirection === 'asc' ? 'desc' : 'asc']; - } - - return [id, 'asc']; - }); - }; - const handleNewButtonClick = useCallback(() => { route.push({ context: 'new' }); }, [route]); @@ -62,18 +30,16 @@ function CustomUserStatusRoute(): ReactNode { route.push({}); }, [route]); - const handleChange = useCallback(() => { - reload(); + const reload = useRef(() => null); + + const handleReload = useCallback(() => { + reload.current(); }, [reload]); if (!canManageUserStatus) { return ; } - if (result.phase === AsyncStatePhase.LOADING || result.phase === AsyncStatePhase.REJECTED) { - return null; - } - return ( @@ -83,30 +49,20 @@ function CustomUserStatusRoute(): ReactNode { - + {context && ( - {context === 'edit' && t('Custom_User_Status_Edit')} - {context === 'new' && t('Custom_User_Status_Add')} + {context === 'edit' ? t('Custom_User_Status_Edit') : t('Custom_User_Status_Add')} - - {context === 'edit' && } - {context === 'new' && } + )} ); -} +}; export default CustomUserStatusRoute; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx new file mode 100644 index 000000000000..958f51f5ad20 --- /dev/null +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx @@ -0,0 +1,37 @@ +import { IUserStatus } from '@rocket.chat/core-typings'; +import { TableRow, TableCell } from '@rocket.chat/fuselage'; +import React, { CSSProperties, ReactElement } from 'react'; + +import MarkdownText from '../../../../components/MarkdownText'; + +const style: CSSProperties = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; + +type CustomUserStatusRowProps = { + status: IUserStatus; + onClick: (id: string) => void; +}; + +const CustomUserStatusRow = ({ status, onClick }: CustomUserStatusRowProps): ReactElement => { + const { _id, name, statusType } = status; + + return ( + onClick(_id)} + onClick={(): void => onClick(_id)} + tabIndex={0} + role='link' + action + qa-user-id={_id} + > + + + + + {statusType} + + + ); +}; + +export default CustomUserStatusRow; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx new file mode 100644 index 000000000000..4407b664695a --- /dev/null +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusTable.tsx @@ -0,0 +1,103 @@ +import { States, StatesIcon, StatesTitle, Pagination } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useState, useMemo, MutableRefObject, useEffect } from 'react'; + +import FilterByText from '../../../../components/FilterByText'; +import { + GenericTable, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableBody, + GenericTableLoadingTable, +} from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../../components/GenericTable/hooks/useSort'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../../lib/asyncState'; +import CustomUserStatusRow from './CustomUserStatusRow'; + +type CustomUserStatusProps = { + reload: MutableRefObject<() => void>; + onClick: (id: string) => void; +}; + +const CustomUserStatus = ({ reload, onClick }: CustomUserStatusProps): ReactElement | null => { + const t = useTranslation(); + const [text, setText] = useState(''); + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + const { sortBy, sortDirection, setSort } = useSort<'name' | 'statusType'>('name'); + + const query = useDebouncedValue( + useMemo( + () => ({ + query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), + sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, + count: itemsPerPage, + offset: current, + }), + [text, itemsPerPage, current, sortBy, sortDirection], + ), + 500, + ); + + const { value, reload: reloadEndpoint, phase } = useEndpointData('custom-user-status.list', query); + + useEffect(() => { + reload.current = reloadEndpoint; + }, [reload, reloadEndpoint]); + + if (phase === AsyncStatePhase.REJECTED) { + return null; + } + + return ( + <> + setText(text)} /> + {value?.statuses.length === 0 && ( + + + {t('No_results_found')} + + )} + {value?.statuses && value.statuses.length > 0 && ( + <> + + + + {t('Name')} + + + {t('Presence')} + + + + {phase === AsyncStatePhase.LOADING && } + {value?.statuses.map((status) => ( + + ))} + + + {phase === AsyncStatePhase.RESOLVED && ( + + )} + + )} + + ); +}; + +export default CustomUserStatus; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/index.ts b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/index.ts new file mode 100644 index 000000000000..814ae75ec5a9 --- /dev/null +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/index.ts @@ -0,0 +1 @@ +export { default } from './CustomUserStatusTable'; diff --git a/apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatus.tsx b/apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatus.tsx deleted file mode 100644 index 5e2d4ff72e5b..000000000000 --- a/apps/meteor/client/views/admin/customUserStatus/EditCustomUserStatus.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Button, ButtonGroup, TextInput, Field, Select, Icon, SelectOption } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState, useMemo, useEffect, ReactElement, SyntheticEvent } from 'react'; - -import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; - -type EditCustomUserStatusProps = { - close: () => void; - onChange: () => void; - data?: { - _id: string; - name: string; - statusType: string; - }; -}; -export function EditCustomUserStatus({ close, onChange, data, ...props }: EditCustomUserStatusProps): ReactElement { - const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - const setModal = useSetModal(); - - const { _id, name: previousName, statusType: previousStatusType } = data || {}; - - const [name, setName] = useState(() => data?.name ?? ''); - const [statusType, setStatusType] = useState(() => data?.statusType ?? ''); - - useEffect(() => { - setName(previousName || ''); - setStatusType(previousStatusType || ''); - }, [previousName, previousStatusType, _id]); - - const saveStatus = useMethod('insertOrUpdateUserStatus'); - const deleteStatus = useMethod('deleteCustomUserStatus'); - - const hasUnsavedChanges = useMemo( - () => previousName !== name || previousStatusType !== statusType, - [name, previousName, previousStatusType, statusType], - ); - const handleSave = useCallback(async () => { - try { - await saveStatus({ - _id, - previousName, - previousStatusType, - name, - statusType, - }); - dispatchToastMessage({ - type: 'success', - message: t('Custom_User_Status_Updated_Successfully'), - }); - onChange(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: String(error) }); - } - }, [saveStatus, _id, previousName, previousStatusType, name, statusType, dispatchToastMessage, t, onChange]); - - const handleDeleteButtonClick = useCallback(() => { - const handleClose = (): void => { - setModal(null); - close(); - onChange(); - }; - - const handleDelete = async (): Promise => { - try { - await deleteStatus(_id); - setModal(() => ( - - {t('Custom_User_Status_Has_Been_Deleted')} - - )); - } catch (error) { - dispatchToastMessage({ type: 'error', message: String(error) }); - onChange(); - } - }; - - const handleCancel = (): void => { - setModal(null); - }; - - setModal(() => ( - - {t('Custom_User_Status_Delete_Warning')} - - )); - }, [_id, close, deleteStatus, dispatchToastMessage, onChange, setModal, t]); - - const presenceOptions: SelectOption[] = [ - ['online', t('Online')], - ['busy', t('Busy')], - ['away', t('Away')], - ['offline', t('Offline')], - ]; - - return ( - - - {t('Name')} - - ): void => setName(e.currentTarget.value)} - placeholder={t('Name')} - /> - - - - {t('Presence')} - -