diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 9f51e36b56eb..ce67c59afa30 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -1,4 +1,4 @@ -import { IRoom } from '@rocket.chat/core-typings'; +import { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -10,7 +10,7 @@ import { useFileInput } from '../../hooks/useFileInput'; import RoomAvatar from './RoomAvatar'; type RoomAvatarEditorProps = { - room: IRoom; + room: Pick; roomAvatar?: string; onChangeAvatar: (url: string | null) => void; }; diff --git a/apps/meteor/client/lib/rooms/roomCoordinator.ts b/apps/meteor/client/lib/rooms/roomCoordinator.ts index 96297baf069f..0a861577f50a 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.ts +++ b/apps/meteor/client/lib/rooms/roomCoordinator.ts @@ -38,7 +38,7 @@ class RoomCoordinatorClient extends RoomCoordinator { getAvatarPath(_room): string { return ''; }, - getIcon(_room: Partial): string | undefined { + getIcon(_room: Partial): IRoomTypeConfig['icon'] { return this.config.icon; }, getUserStatus(_roomId: string): string | undefined { @@ -92,7 +92,7 @@ class RoomCoordinatorClient extends RoomCoordinator { openRoom(type, name, render); } - getIcon(room: Partial): string | undefined { + getIcon(room: Partial): IRoomTypeConfig['icon'] { return room?.t && this.getRoomDirectives(room.t)?.getIcon(room); } diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.js b/apps/meteor/client/views/admin/rooms/EditRoom.tsx similarity index 79% rename from apps/meteor/client/views/admin/rooms/EditRoom.js rename to apps/meteor/client/views/admin/rooms/EditRoom.tsx index a5f210e110a6..029c1fbb251b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.js +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -1,3 +1,4 @@ +import { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { Box, Button, ButtonGroup, TextInput, Field, ToggleSwitch, Icon, TextAreaInput } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -9,7 +10,7 @@ import { useMethod, useTranslation, } from '@rocket.chat/ui-contexts'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, ReactElement } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import GenericModal from '../../../components/GenericModal'; @@ -18,10 +19,30 @@ import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointActionExperimental } from '../../../hooks/useEndpointActionExperimental'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModal from '../../teams/contextualBar/info/Delete/DeleteTeamModal'; +import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/Delete'; -const getInitialValues = (room) => ({ - roomName: room.t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(room.t, { type: room.t, ...room }), +type EditRoomProps = { + room: Pick; + onChange: () => void; + onDelete: () => void; +}; + +type EditRoomFormValues = { + roomName: IRoom['name']; + roomTopic: string; + roomType: IRoom['t']; + readOnly: boolean; + isDefault: boolean; + favorite: boolean; + featured: boolean; + roomDescription: string; + roomAnnouncement: string; + roomAvatar: IRoom['avatarETag']; + archived: boolean; +}; + +const getInitialValues = (room: Pick): EditRoomFormValues => ({ + roomName: room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room), roomType: room.t, readOnly: !!room.ro, archived: !!room.archived, @@ -34,26 +55,27 @@ const getInitialValues = (room) => ({ roomAvatar: undefined, }); -function EditRoom({ room, onChange, onDelete }) { +const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); const [deleting, setDeleting] = useState(false); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); + const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = useMemo(() => { const isAllowed = roomCoordinator.getRoomDirectives(room.t)?.allowRoomSettingChange; return [ - isAllowed(room, RoomSettingsEnum.NAME), - isAllowed(room, RoomSettingsEnum.TOPIC), - isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), - isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), - isAllowed(room, RoomSettingsEnum.DESCRIPTION), - isAllowed(room, RoomSettingsEnum.TYPE), - isAllowed(room, RoomSettingsEnum.READ_ONLY), + isAllowed?.(room, RoomSettingsEnum.NAME), + isAllowed?.(room, RoomSettingsEnum.TOPIC), + isAllowed?.(room, RoomSettingsEnum.ANNOUNCEMENT), + isAllowed?.(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), + isAllowed?.(room, RoomSettingsEnum.DESCRIPTION), + isAllowed?.(room, RoomSettingsEnum.TYPE), + isAllowed?.(room, RoomSettingsEnum.READ_ONLY), ]; }, [room]); @@ -69,7 +91,7 @@ function EditRoom({ room, onChange, onDelete }) { roomAvatar, roomDescription, roomAnnouncement, - } = values; + } = values as EditRoomFormValues; const { handleIsDefault, @@ -98,7 +120,7 @@ function EditRoom({ room, onChange, onDelete }) { const archiveAction = useEndpointActionExperimental('POST', 'rooms.changeArchivationState', t(archiveMessage)); const handleSave = useMutableCallback(async () => { - const save = () => + const save = (): Promise<{ success: boolean; rid: string }> => saveAction({ rid: room._id, roomName: roomType === 'd' ? undefined : roomName, @@ -113,9 +135,12 @@ function EditRoom({ room, onChange, onDelete }) { roomAvatar, }); - const archive = () => archiveAction({ rid: room._id, action: archiveSelector }); + const archive = (): Promise<{ success: boolean }> => archiveAction({ rid: room._id, action: archiveSelector }); - await Promise.all([hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean)); + const promises = []; + hasUnsavedChanges && promises.push(save()); + changeArchivation && promises.push(archive()); + await Promise.all(promises); onChange(); }); @@ -129,25 +154,25 @@ function EditRoom({ room, onChange, onDelete }) { const handleDelete = useMutableCallback(() => { if (room.teamMain) { setModal( - { - const roomsToRemove = Array.isArray(deletedRooms) && deletedRooms.length > 0 ? deletedRooms : []; + => { + const roomsToRemove = Array.isArray(deletedRooms) && deletedRooms.length > 0 ? deletedRooms.map((room) => room._id) : []; try { setDeleting(true); setModal(null); - await deleteTeam({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); + await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); roomsRoute.push({}); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: String(error) }); setDeleting(false); } finally { onDelete(); } }} - onCancel={() => setModal(null)} - teamId={room.teamId} + onCancel={(): void => setModal(null)} + teamId={room.teamId as string} />, ); @@ -157,7 +182,7 @@ function EditRoom({ room, onChange, onDelete }) { setModal( { + onConfirm={async (): Promise => { try { setDeleting(true); setModal(null); @@ -165,13 +190,14 @@ function EditRoom({ room, onChange, onDelete }) { dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); roomsRoute.push({}); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: String(error) }); setDeleting(false); } finally { onDelete(); } }} - onCancel={() => setModal(null)} + onClose={(): void => setModal(null)} + onCancel={(): void => setModal(null)} confirmText={t('Yes_delete_it')} > {t('Delete_Room_Warning')} @@ -304,6 +330,6 @@ function EditRoom({ room, onChange, onDelete }) { ); -} +}; export default EditRoom; diff --git a/apps/meteor/client/views/admin/rooms/EditRoomContextBar.js b/apps/meteor/client/views/admin/rooms/EditRoomContextBar.tsx similarity index 71% rename from apps/meteor/client/views/admin/rooms/EditRoomContextBar.js rename to apps/meteor/client/views/admin/rooms/EditRoomContextBar.tsx index 5bbcb05ea3d4..87f1368909c2 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoomContextBar.js +++ b/apps/meteor/client/views/admin/rooms/EditRoomContextBar.tsx @@ -1,12 +1,12 @@ import { usePermission } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditRoomWithData from './EditRoomWithData'; -function EditRoomContextBar({ rid, onReload }) { +const EditRoomContextBar = ({ rid, onReload }: { rid: string | undefined; onReload: () => void }): ReactElement => { const canViewRoomAdministration = usePermission('view-room-administration'); return canViewRoomAdministration ? : ; -} +}; export default EditRoomContextBar; diff --git a/apps/meteor/client/views/admin/rooms/EditRoomWithData.js b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx similarity index 67% rename from apps/meteor/client/views/admin/rooms/EditRoomWithData.js rename to apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx index cedf9bbdbe43..95f822faca9b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoomWithData.js +++ b/apps/meteor/client/views/admin/rooms/EditRoomWithData.tsx @@ -1,13 +1,13 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; +import React, { useMemo, FC } from 'react'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import EditRoom from './EditRoom'; -function EditRoomWithData({ rid, onReload }) { +const EditRoomWithData: FC<{ rid?: string; onReload: () => void }> = ({ rid, onReload }) => { const { - value: data = {}, + value: data, phase: state, error, reload, @@ -30,19 +30,19 @@ function EditRoomWithData({ rid, onReload }) { } if (state === AsyncStatePhase.REJECTED) { - return error.message; + return <>{error?.message}; } - const handleChange = () => { + const handleChange = (): void => { reload(); onReload(); }; - const handleDelete = () => { + const handleDelete = (): void => { onReload(); }; - return ; -} + return data ? : null; +}; export default EditRoomWithData; diff --git a/apps/meteor/client/views/admin/rooms/FilterByTypeAndText.js b/apps/meteor/client/views/admin/rooms/FilterByTypeAndText.tsx similarity index 72% rename from apps/meteor/client/views/admin/rooms/FilterByTypeAndText.js rename to apps/meteor/client/views/admin/rooms/FilterByTypeAndText.tsx index ca59abae0396..a37657c36e3c 100644 --- a/apps/meteor/client/views/admin/rooms/FilterByTypeAndText.js +++ b/apps/meteor/client/views/admin/rooms/FilterByTypeAndText.tsx @@ -1,7 +1,7 @@ import { Box, Icon, TextInput, Field, CheckBox, Margins } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, ReactElement, Dispatch, SetStateAction } from 'react'; export const DEFAULT_TYPES = ['d', 'p', 'c', 'teams']; @@ -14,7 +14,7 @@ export const roomTypeI18nMap = { team: 'Team', }; -const FilterByTypeAndText = ({ setFilter, ...props }) => { +const FilterByTypeAndText = ({ setFilter, ...props }: { setFilter?: Dispatch> }): ReactElement => { const [text, setText] = useState(''); const [types, setTypes] = useState({ d: false, @@ -28,16 +28,16 @@ const FilterByTypeAndText = ({ setFilter, ...props }) => { const t = useTranslation(); const handleChange = useCallback((event) => setText(event.currentTarget.value), []); - const handleCheckBox = useCallback((type) => setTypes({ ...types, [type]: !types[type] }), [types]); + const handleCheckBox = useCallback((type: keyof typeof types) => setTypes({ ...types, [type]: !types[type] }), [types]); useEffect(() => { if (Object.values(types).filter(Boolean).length === 0) { - return setFilter({ text, types: DEFAULT_TYPES }); + return setFilter?.({ text, types: DEFAULT_TYPES }); } const _types = Object.entries(types) .filter(([, value]) => Boolean(value)) .map(([key]) => key); - setFilter({ text, types: _types }); + setFilter?.({ text, types: _types }); }, [setFilter, text, types]); const idDirect = useUniqueId(); @@ -60,27 +60,27 @@ const FilterByTypeAndText = ({ setFilter, ...props }) => { - handleCheckBox('d')} /> + handleCheckBox('d')} /> {t('Direct')} - handleCheckBox('c')} /> + handleCheckBox('c')} /> {t('Public')} - handleCheckBox('p')} /> + handleCheckBox('p')} /> {t('Private')} - handleCheckBox('l')} /> + handleCheckBox('l')} /> {t('Omnichannel')} - handleCheckBox('discussions')} /> + handleCheckBox('discussions')} /> {t('Discussions')} - handleCheckBox('teams')} /> + handleCheckBox('teams')} /> {t('Teams')} diff --git a/apps/meteor/client/views/admin/rooms/RoomsPage.js b/apps/meteor/client/views/admin/rooms/RoomsPage.js deleted file mode 100644 index ed3b62c1a80a..000000000000 --- a/apps/meteor/client/views/admin/rooms/RoomsPage.js +++ /dev/null @@ -1,74 +0,0 @@ -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useState, useMemo } from 'react'; - -import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import EditRoomContextBar from './EditRoomContextBar'; -import RoomsTable from './RoomsTable'; - -export const DEFAULT_TYPES = ['d', 'p', 'c', 'teams']; - -const useQuery = ({ text, types, itemsPerPage, current }, [column, direction]) => - useMemo( - () => ({ - filter: text || '', - types, - sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, types, itemsPerPage, current, column, direction], - ); - -export function RoomsPage() { - const t = useTranslation(); - - const context = useRouteParameter('context'); - const id = useRouteParameter('id'); - - const roomsRoute = useRoute('admin-rooms'); - - const handleVerticalBarCloseButtonClick = () => { - roomsRoute.push({}); - }; - - const [params, setParams] = useState({ - text: '', - types: DEFAULT_TYPES, - current: 0, - itemsPerPage: 25, - }); - const [sort, setSort] = useState(['name', 'asc']); - - const debouncedParams = useDebouncedValue(params, 500); - const debouncedSort = useDebouncedValue(sort, 500); - - const query = useQuery(debouncedParams, debouncedSort); - - const endpointData = useEndpointData('rooms.adminRooms', query); - - return ( - - - - - - - - {context && ( - - - {t('Room_Info')} - - - - - - )} - - ); -} - -export default RoomsPage; diff --git a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx new file mode 100644 index 000000000000..cb1f3d927770 --- /dev/null +++ b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx @@ -0,0 +1,45 @@ +import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useRef, ReactElement } from 'react'; + +import Page from '../../../components/Page'; +import VerticalBar from '../../../components/VerticalBar'; +import EditRoomContextBar from './EditRoomContextBar'; +import RoomsTable from './RoomsTable'; + +const RoomsPage = (): ReactElement => { + const t = useTranslation(); + + const context = useRouteParameter('context'); + const id = useRouteParameter('id'); + + const roomsRoute = useRoute('admin-rooms'); + + const handleVerticalBarCloseButtonClick = (): void => { + roomsRoute.push({}); + }; + + const reloadRef = useRef(() => null); + + return ( + + + + + + + + {context && ( + + + {t('Room_Info')} + + + + + + )} + + ); +}; + +export default RoomsPage; diff --git a/apps/meteor/client/views/admin/rooms/RoomsRoute.js b/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx similarity index 80% rename from apps/meteor/client/views/admin/rooms/RoomsRoute.js rename to apps/meteor/client/views/admin/rooms/RoomsRoute.tsx index 89f652975a55..880df4034429 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsRoute.js +++ b/apps/meteor/client/views/admin/rooms/RoomsRoute.tsx @@ -1,10 +1,10 @@ import { usePermission } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import RoomsPage from './RoomsPage'; -function RoomsRoute() { +const RoomsRoute = (): ReactElement => { const canViewRoomAdministration = usePermission('view-room-administration'); if (!canViewRoomAdministration) { @@ -12,6 +12,6 @@ function RoomsRoute() { } return ; -} +}; export default RoomsRoute; diff --git a/apps/meteor/client/views/admin/rooms/RoomsTable.js b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx similarity index 59% rename from apps/meteor/client/views/admin/rooms/RoomsTable.js rename to apps/meteor/client/views/admin/rooms/RoomsTable.tsx index 42d0c5f2cc73..eebd3b2e1790 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTable.js +++ b/apps/meteor/client/views/admin/rooms/RoomsTable.tsx @@ -1,15 +1,57 @@ +import { IRoom } from '@rocket.chat/core-typings'; import { Box, Table, Icon } from '@rocket.chat/fuselage'; -import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useMediaQuery, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, CSSProperties, ReactElement, MutableRefObject } from 'react'; import GenericTable from '../../../components/GenericTable'; import RoomAvatar from '../../../components/avatar/RoomAvatar'; +import { useEndpointData } from '../../../hooks/useEndpointData'; import { AsyncStatePhase } from '../../../lib/asyncState'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import FilterByTypeAndText from './FilterByTypeAndText'; -const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; +type RoomParamsType = { + text?: string; + types?: string[]; + current?: number; + itemsPerPage?: 25 | 50 | 100; +}; + +const style: CSSProperties = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; + +export const DEFAULT_TYPES = ['d', 'p', 'c', 'teams']; + +const useQuery = ( + { + text, + types, + itemsPerPage, + current, + }: { + text?: string; + types?: string[]; + itemsPerPage?: 25 | 50 | 100; + current?: number; + }, + [column, direction]: [string, 'asc' | 'desc'], +): { + filter: string; + types: string[]; + sort: string; + count?: number; + offset?: number; +} => + useMemo( + () => ({ + filter: text || '', + types: types || [], + sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), + ...(itemsPerPage && { count: itemsPerPage }), + ...(current && { offset: current }), + }), + [text, types, itemsPerPage, current, column, direction], + ); export const roomTypeI18nMap = { l: 'Omnichannel', @@ -17,18 +59,19 @@ export const roomTypeI18nMap = { d: 'Direct', p: 'Group', discussion: 'Discussion', -}; +} as const; -const getRoomType = (room) => { +const getRoomType = (room: IRoom): typeof roomTypeI18nMap[keyof typeof roomTypeI18nMap] | 'Teams_Public_Team' | 'Teams_Private_Team' => { if (room.teamMain) { return room.t === 'c' ? 'Teams_Public_Team' : 'Teams_Private_Team'; } - return roomTypeI18nMap[room.t]; + return roomTypeI18nMap[room.t as keyof typeof roomTypeI18nMap]; }; -const getRoomDisplayName = (room) => (room.t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(room.t, room)); +const getRoomDisplayName = (room: IRoom): string | undefined => + room.t === 'd' ? room.usernames?.join(' x ') : roomCoordinator.getRoomName(room.t, room); -const useDisplayData = (asyncState, sort) => +const useDisplayData = (asyncState: any, sort: [string, 'asc' | 'desc']): IRoom[] => useMemo(() => { const { value = {}, phase } = asyncState; @@ -37,9 +80,9 @@ const useDisplayData = (asyncState, sort) => } if (sort[0] === 'name' && value.rooms) { - return value.rooms.sort((a, b) => { - const aName = getRoomDisplayName(a); - const bName = getRoomDisplayName(b); + return value.rooms.sort((a: IRoom, b: IRoom) => { + const aName = getRoomDisplayName(a) || ''; + const bName = getRoomDisplayName(b) || ''; if (aName === bName) { return 0; } @@ -50,19 +93,37 @@ const useDisplayData = (asyncState, sort) => return value.rooms; }, [asyncState, sort]); -function RoomsTable({ endpointData, params, onChangeParams, sort, onChangeSort }) { +const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement => { const t = useTranslation(); - const mediaQuery = useMediaQuery('(min-width: 1024px)'); const routeName = 'admin-rooms'; - const { value: data = {} } = endpointData; + const [params, setParams] = useState({ + text: '', + types: DEFAULT_TYPES, + current: 0, + itemsPerPage: 25, + }); + const [sort, setSort] = useState<[string, 'asc' | 'desc']>(['name', 'asc']); + + const debouncedParams = useDebouncedValue(params, 500); + const debouncedSort = useDebouncedValue(sort, 500); + + const query = useQuery(debouncedParams, debouncedSort); + + const endpointData = useEndpointData('rooms.adminRooms', query); + + const { value: data, reload: reloadEndPoint } = endpointData; + + useEffect(() => { + reload.current = reloadEndPoint; + }, [reload, reloadEndPoint]); const router = useRoute(routeName); const onClick = useCallback( - (rid) => () => + (rid) => (): void => router.push({ context: 'edit', id: rid, @@ -75,12 +136,12 @@ function RoomsTable({ endpointData, params, onChangeParams, sort, onChangeSort } const [sortBy, sortDirection] = sort; if (sortBy === id) { - onChangeSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); + setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); return; } - onChangeSort([id, 'asc']); + setSort([id, 'asc']); }, - [sort, onChangeSort], + [sort, setSort], ); const displayData = useDisplayData(endpointData, sort); @@ -146,7 +207,7 @@ function RoomsTable({ endpointData, params, onChangeParams, sort, onChangeSort } const renderRow = useCallback( (room) => { - const { _id, name, t: type, usersCount, msgs, default: isDefault, featured, usernames, ...args } = room; + const { _id, t: type, usersCount, msgs, default: isDefault, featured, ...args } = room; const icon = roomCoordinator.getIcon(room); const roomName = getRoomDisplayName(room); @@ -157,7 +218,7 @@ function RoomsTable({ endpointData, params, onChangeParams, sort, onChangeSort } - + {icon && } {roomName} @@ -186,12 +247,12 @@ function RoomsTable({ endpointData, params, onChangeParams, sort, onChangeSort } header={header} renderRow={renderRow} results={displayData} - total={data.total} - setParams={onChangeParams} + total={data?.total} params={params} - renderFilter={({ onChange, ...props }) => } + setParams={setParams} + renderFilter={({ onChange, ...props }): ReactElement => } /> ); -} +}; export default RoomsTable; diff --git a/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.js b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.js index e5b356b0a3ef..28ddbb624fa5 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.js +++ b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.js @@ -1,7 +1,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState, useCallback } from 'react'; -import { StepOne, StepTwo } from '.'; +import StepOne from './StepOne'; +import StepTwo from './StepTwo'; const STEPS = { LIST_ROOMS: 'LIST_ROOMS', CONFIRM_DELETE: 'CONFIRM_DELETE' }; diff --git a/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.stories.tsx b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.stories.tsx index 3b079ef9801a..d0911836922c 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.stories.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModal.stories.tsx @@ -2,7 +2,9 @@ import { action } from '@storybook/addon-actions'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import DeleteTeamModal, { StepOne, StepTwo } from '.'; +import DeleteTeamModal from '.'; +import StepOne from './StepOne'; +import StepTwo from './StepTwo'; export default { title: 'Teams/Contextual Bar/DeleteTeamModal', diff --git a/apps/meteor/client/views/teams/contextualBar/info/Delete/index.js b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModalWithRooms.tsx similarity index 77% rename from apps/meteor/client/views/teams/contextualBar/info/Delete/index.js rename to apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModalWithRooms.tsx index c935ab038f66..3625da876daa 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/Delete/index.js +++ b/apps/meteor/client/views/teams/contextualBar/info/Delete/DeleteTeamModalWithRooms.tsx @@ -1,15 +1,20 @@ +import { IRoom } from '@rocket.chat/core-typings'; import { Skeleton } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; +import React, { useMemo, ReactElement } from 'react'; import GenericModal from '../../../../../components/GenericModal'; import { AsyncStatePhase } from '../../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import DeleteTeamModal from './DeleteTeamModal'; -import StepOne from './StepOne'; -import StepTwo from './StepTwo'; -const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }) => { +type DeleteTeamModalWithRoomsProps = { + teamId: string; + onConfirm: (rooms: IRoom[]) => void; + onCancel: () => void; +}; + +const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }: DeleteTeamModalWithRoomsProps): ReactElement => { const { value, phase } = useEndpointData( 'teams.listRooms', useMemo(() => ({ teamId }), [teamId]), @@ -27,6 +32,4 @@ const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }) => { return ; }; -export { StepOne, StepTwo }; - export default DeleteTeamModalWithRooms; diff --git a/apps/meteor/client/views/teams/contextualBar/info/Delete/index.ts b/apps/meteor/client/views/teams/contextualBar/info/Delete/index.ts new file mode 100644 index 000000000000..256a9f44f9a6 --- /dev/null +++ b/apps/meteor/client/views/teams/contextualBar/info/Delete/index.ts @@ -0,0 +1 @@ +export { default } from './DeleteTeamModalWithRooms'; diff --git a/apps/meteor/definition/IRoomTypeConfig.ts b/apps/meteor/definition/IRoomTypeConfig.ts index 4201c38c3627..4b2689996d13 100644 --- a/apps/meteor/definition/IRoomTypeConfig.ts +++ b/apps/meteor/definition/IRoomTypeConfig.ts @@ -48,7 +48,7 @@ export const UiTextContext = { export interface IRoomTypeConfig { identifier: string; order: number; - icon?: string; + icon?: 'hash' | 'hashtag' | 'hashtag-lock' | 'at' | 'omnichannel' | 'phone' | 'star'; header?: string; label?: string; route?: IRoomTypeRouteConfig; @@ -70,7 +70,7 @@ export interface IRoomTypeClientDirectives { getAvatarPath: ( room: AtLeast & { username?: IRoom['_id'] }, ) => string; - getIcon: (room: Partial) => string | undefined; + getIcon: (room: Partial) => IRoomTypeConfig['icon']; getUserStatus: (roomId: string) => string | undefined; findRoom: (identifier: string) => IRoom | undefined; showJoinLink: (roomId: string) => boolean; diff --git a/apps/meteor/lib/rooms/adminFields.ts b/apps/meteor/lib/rooms/adminFields.ts index dba428695fde..3375e44eaa1d 100644 --- a/apps/meteor/lib/rooms/adminFields.ts +++ b/apps/meteor/lib/rooms/adminFields.ts @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; export const adminFields: Partial> = { + _id: 1, prid: 1, fname: 1, name: 1, diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 3baaa21deec4..abfd9ba8a941 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1443,6 +1443,7 @@ "Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled", "Details": "Details", "Different_Style_For_User_Mentions": "Different style for user mentions", + "Direct": "Direct", "Direct_Message": "Direct Message", "Direct_message_creation_description": "You are about to create a chat with multiple users. Add the ones you would like to talk, everyone in the same place, using direct messages.", "Direct_message_someone": "Direct message someone", @@ -1896,6 +1897,7 @@ "Favorite": "Favorite", "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", + "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings.
For **Jitsi**, please make sure you have Jitsi Enabled under Admin -> Video Conference -> Jitsi -> Enabled.
For **WebRTC**, please make sure you have WebRTC enabled under Admin -> WebRTC -> Enabled.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.", "Feature_Limiting": "Feature Limiting", @@ -2137,6 +2139,7 @@ "Graphql_CORS": "GraphQL CORS", "Graphql_Enabled": "GraphQL Enabled", "Graphql_Subscription_Port": "GraphQL Subscription Port", + "Group": "Group", "Group_by": "Group by", "Group_by_Type": "Group by Type", "Group_discussions": "Group discussions", @@ -3376,6 +3379,7 @@ "Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.", "Output_format": "Output format", "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Override URL to which files are uploaded. This url also used for downloads unless a CDN is given", + "Owner": "Owner", "Play": "Play", "Page_title": "Page title", "Page_URL": "Page URL", diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 4a663543a2a7..14b3f9a0ba1a 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -234,3 +234,31 @@ export const isOmnichannelRoomFromAppSource = (room: IRoom): room is IOmnichanne return room.source?.type === OmnichannelSourceType.APP; }; + +export type RoomAdminFieldsType = + | '_id' + | 'prid' + | 'fname' + | 'name' + | 't' + | 'cl' + | 'u' + | 'usernames' + | 'usersCount' + | 'muted' + | 'unmuted' + | 'ro' + | 'default' + | 'favorite' + | 'featured' + | 'topic' + | 'msgs' + | 'archived' + | 'tokenpass' + | 'teamId' + | 'teamMain' + | 'announcement' + | 'description' + | 'broadcast' + | 'uids' + | 'avatarETag'; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 0352d63f9882..647127c4b469 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -1,4 +1,7 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser, RoomAdminFieldsType } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; +import type { PaginatedResult } from '../helpers/PaginatedResult'; export type RoomsEndpoints = { 'rooms.autocomplete.channelAndPrivate': { @@ -53,4 +56,44 @@ export type RoomsEndpoints = { success: boolean; }; }; + 'rooms.adminRooms': { + GET: ( + params: PaginatedRequest<{ + filter?: string; + types?: string[]; + }>, + ) => PaginatedResult<{ rooms: Pick[] }>; + }; + 'rooms.adminRooms.getRoom': { + GET: (params: { rid?: string }) => Pick; + }; + 'rooms.saveRoomSettings': { + POST: (params: { + rid: string; + roomAvatar?: string; + featured?: boolean; + roomName?: string; + roomTopic?: string; + roomAnnouncement?: string; + roomDescription?: string; + roomType?: IRoom['t']; + readOnly?: boolean; + reactWhenReadOnly?: boolean; + default?: boolean; + tokenpass?: string; + encrypted?: boolean; + favorite?: { + defaultValue?: boolean; + favorite?: boolean; + }; + }) => { + success: boolean; + rid: string; + }; + }; + 'rooms.changeArchivationState': { + POST: (params: { rid: string; action?: string }) => { + success: boolean; + }; + }; };