From bdeb54a0406342bfcdafcec687d60b70b911c5f6 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Tue, 17 May 2022 00:12:55 +0530 Subject: [PATCH 1/6] Chore: Convert Admin -> Rooms to TS (#25348) Co-authored-by: Guilherme Gazzo --- .../components/avatar/RoomAvatarEditor.tsx | 4 +- .../client/lib/rooms/roomCoordinator.ts | 4 +- .../admin/rooms/{EditRoom.js => EditRoom.tsx} | 80 ++++++++----- ...omContextBar.js => EditRoomContextBar.tsx} | 6 +- ...itRoomWithData.js => EditRoomWithData.tsx} | 16 +-- ...TypeAndText.js => FilterByTypeAndText.tsx} | 22 ++-- .../client/views/admin/rooms/RoomsPage.js | 74 ------------ .../client/views/admin/rooms/RoomsPage.tsx | 45 ++++++++ .../rooms/{RoomsRoute.js => RoomsRoute.tsx} | 6 +- .../rooms/{RoomsTable.js => RoomsTable.tsx} | 109 ++++++++++++++---- .../info/Delete/DeleteTeamModal.js | 3 +- .../info/Delete/DeleteTeamModal.stories.tsx | 4 +- ...{index.js => DeleteTeamModalWithRooms.tsx} | 15 ++- .../teams/contextualBar/info/Delete/index.ts | 1 + apps/meteor/definition/IRoomTypeConfig.ts | 4 +- apps/meteor/lib/rooms/adminFields.ts | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 4 + packages/core-typings/src/IRoom.ts | 28 +++++ packages/rest-typings/src/v1/rooms.ts | 45 +++++++- 19 files changed, 306 insertions(+), 165 deletions(-) rename apps/meteor/client/views/admin/rooms/{EditRoom.js => EditRoom.tsx} (79%) rename apps/meteor/client/views/admin/rooms/{EditRoomContextBar.js => EditRoomContextBar.tsx} (71%) rename apps/meteor/client/views/admin/rooms/{EditRoomWithData.js => EditRoomWithData.tsx} (67%) rename apps/meteor/client/views/admin/rooms/{FilterByTypeAndText.js => FilterByTypeAndText.tsx} (72%) delete mode 100644 apps/meteor/client/views/admin/rooms/RoomsPage.js create mode 100644 apps/meteor/client/views/admin/rooms/RoomsPage.tsx rename apps/meteor/client/views/admin/rooms/{RoomsRoute.js => RoomsRoute.tsx} (80%) rename apps/meteor/client/views/admin/rooms/{RoomsTable.js => RoomsTable.tsx} (59%) rename apps/meteor/client/views/teams/contextualBar/info/Delete/{index.js => DeleteTeamModalWithRooms.tsx} (77%) create mode 100644 apps/meteor/client/views/teams/contextualBar/info/Delete/index.ts 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; + }; + }; }; From 75c7f4d0e19fc74ddf151b60845a12f03c845695 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 16 May 2022 16:10:42 -0300 Subject: [PATCH 2/6] [FIX] UI/UX issues on Live Chat widget (#25407) * [FIX] UI/UX issues on Live Chat widget * Use @rocketchat/logo * READ THE ERRORS MARTIN * Remove old logo * Fix Reviews * chore: remove unnecessary class prop Co-authored-by: dougfabris --- packages/livechat/package.json | 2 ++ .../livechat/src/components/Footer/index.js | 6 +++--- .../livechat/src/components/Footer/logo.svg | 21 ------------------- .../src/components/Footer/styles.scss | 20 +++++++----------- .../uiKit/message/PlainText/styles.scss | 2 ++ yarn.lock | 2 ++ 6 files changed, 17 insertions(+), 36 deletions(-) delete mode 100644 packages/livechat/src/components/Footer/logo.svg diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 358c0bd56867..a7135de9f2bb 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -84,6 +84,8 @@ }, "dependencies": { "@kossnocorp/desvg": "^0.2.0", + "@rocket.chat/fuselage-tokens": "~0.31.11", + "@rocket.chat/logo": "^0.31.11", "@rocket.chat/sdk": "^1.0.0-alpha.42", "@rocket.chat/ui-kit": "^0.14.1", "crypto-js": "^4.1.1", diff --git a/packages/livechat/src/components/Footer/index.js b/packages/livechat/src/components/Footer/index.js index 88c1fac5c00d..98d21cb95f5c 100644 --- a/packages/livechat/src/components/Footer/index.js +++ b/packages/livechat/src/components/Footer/index.js @@ -1,8 +1,8 @@ +import { RocketChatLogo } from '@rocket.chat/logo'; import { withTranslation } from 'react-i18next'; import { PopoverMenu } from '../Menu'; import { createClassName } from '../helpers'; -import Logo from './logo.svg'; import styles from './styles.scss'; @@ -23,8 +23,8 @@ export const FooterContent = ({ children, className, ...props }) => ( export const PoweredBy = withTranslation()(({ className, t, ...props }) => (

{t('powered_by_rocket_chat').split('Rocket.Chat')[0]} - - + + {t('powered_by_rocket_chat').split('Rocket.Chat')[1]}

diff --git a/packages/livechat/src/components/Footer/logo.svg b/packages/livechat/src/components/Footer/logo.svg deleted file mode 100644 index 2d943df79795..000000000000 --- a/packages/livechat/src/components/Footer/logo.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/livechat/src/components/Footer/styles.scss b/packages/livechat/src/components/Footer/styles.scss index 3af8365a2003..757e4f518df7 100644 --- a/packages/livechat/src/components/Footer/styles.scss +++ b/packages/livechat/src/components/Footer/styles.scss @@ -73,24 +73,20 @@ align-self: flex-end; .powered-by__logo { + display: inline-flex; + + height: 10px; margin: 0 5px; vertical-align: middle; + align-items: center; - :global(.text) { + svg { fill: #{$color-text-grey}; - } - - &:hover :global(.text) { - fill: #2f343d; - } - - :global(.rocket) { - fill: #{$color-text-grey}; - } - &:hover :global(.rocket) { - fill: #db2323; + &:hover { + fill: #db2323; + } } } } diff --git a/packages/livechat/src/components/uiKit/message/PlainText/styles.scss b/packages/livechat/src/components/uiKit/message/PlainText/styles.scss index 960f54e9719d..5cbbd087bd34 100644 --- a/packages/livechat/src/components/uiKit/message/PlainText/styles.scss +++ b/packages/livechat/src/components/uiKit/message/PlainText/styles.scss @@ -1,4 +1,6 @@ .uikit-plain-text { + text-align: left; + white-space: normal; word-wrap: break-word; word-break: break-word; } diff --git a/yarn.lock b/yarn.lock index 5276084b95bb..2aa4c15510e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3864,6 +3864,8 @@ __metadata: "@babel/preset-env": ^7.11.5 "@kossnocorp/desvg": ^0.2.0 "@rocket.chat/eslint-config": ^0.4.0 + "@rocket.chat/fuselage-tokens": ~0.31.11 + "@rocket.chat/logo": ^0.31.11 "@rocket.chat/sdk": ^1.0.0-alpha.42 "@rocket.chat/ui-kit": ^0.14.1 "@storybook/addon-actions": ^6.0.12 From 99a3651f56489b428d08cdcb262dba32bb77c39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 16 May 2022 16:56:01 -0300 Subject: [PATCH 3/6] Chore: Convert apps/meteor/client/views/admin/settings/inputs folder (#25427) --- .../settings/inputs/ActionSettingInput.js | 31 -------------- .../inputs/ActionSettingInput.stories.tsx | 14 ++++--- .../settings/inputs/ActionSettingInput.tsx | 42 +++++++++++++++++++ .../inputs/AssetSettingInput.stories.tsx | 2 +- ...tSettingInput.js => AssetSettingInput.tsx} | 38 ++++++++++------- ...ettingInput.js => BooleanSettingInput.tsx} | 27 ++++++++++-- .../inputs/{CodeMirror.js => CodeMirror.tsx} | 41 ++++++++++++++---- ...deSettingInput.js => CodeSettingInput.tsx} | 22 ++++++++-- ...rSettingInput.js => ColorSettingInput.tsx} | 24 +++++++++-- ...ntSettingInput.js => FontSettingInput.tsx} | 20 +++++++-- ...ettingInput.js => GenericSettingInput.tsx} | 20 +++++++-- .../inputs/IntSettingInput.stories.tsx | 2 +- ...IntSettingInput.js => IntSettingInput.tsx} | 21 ++++++++-- .../inputs/LanguageSettingInput.stories.tsx | 6 +-- ...ttingInput.js => LanguageSettingInput.tsx} | 21 ++++++++-- .../MultiSelectSettingInput.stories.tsx | 16 ++++--- ...ngInput.js => MultiSelectSettingInput.tsx} | 26 +++++++++--- ...ttingInput.js => PasswordSettingInput.tsx} | 21 ++++++++-- ...ngInput.js => RelativeUrlSettingInput.tsx} | 23 +++++++--- ...ttingInput.js => RoomPickSettingInput.tsx} | 36 ++++++++++------ .../inputs/SelectSettingInput.stories.tsx | 25 +++++------ ...SettingInput.js => SelectSettingInput.tsx} | 23 ++++++++-- ...nput.js => SelectTimezoneSettingInput.tsx} | 21 ++++++++-- ...SettingInput.js => StringSettingInput.tsx} | 22 ++++++++-- 24 files changed, 389 insertions(+), 155 deletions(-) delete mode 100644 apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.js create mode 100644 apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.tsx rename apps/meteor/client/views/admin/settings/inputs/{AssetSettingInput.js => AssetSettingInput.tsx} (63%) rename apps/meteor/client/views/admin/settings/inputs/{BooleanSettingInput.js => BooleanSettingInput.tsx} (52%) rename apps/meteor/client/views/admin/settings/inputs/{CodeMirror.js => CodeMirror.tsx} (64%) rename apps/meteor/client/views/admin/settings/inputs/{CodeSettingInput.js => CodeSettingInput.tsx} (75%) rename apps/meteor/client/views/admin/settings/inputs/{ColorSettingInput.js => ColorSettingInput.tsx} (77%) rename apps/meteor/client/views/admin/settings/inputs/{FontSettingInput.js => FontSettingInput.tsx} (63%) rename apps/meteor/client/views/admin/settings/inputs/{GenericSettingInput.js => GenericSettingInput.tsx} (63%) rename apps/meteor/client/views/admin/settings/inputs/{IntSettingInput.js => IntSettingInput.tsx} (63%) rename apps/meteor/client/views/admin/settings/inputs/{LanguageSettingInput.js => LanguageSettingInput.tsx} (68%) rename apps/meteor/client/views/admin/settings/inputs/{MultiSelectSettingInput.js => MultiSelectSettingInput.tsx} (65%) rename apps/meteor/client/views/admin/settings/inputs/{PasswordSettingInput.js => PasswordSettingInput.tsx} (61%) rename apps/meteor/client/views/admin/settings/inputs/{RelativeUrlSettingInput.js => RelativeUrlSettingInput.tsx} (62%) rename apps/meteor/client/views/admin/settings/inputs/{RoomPickSettingInput.js => RoomPickSettingInput.tsx} (68%) rename apps/meteor/client/views/admin/settings/inputs/{SelectSettingInput.js => SelectSettingInput.tsx} (64%) rename apps/meteor/client/views/admin/settings/inputs/{SelectTimezoneSettingInput.js => SelectTimezoneSettingInput.tsx} (68%) rename apps/meteor/client/views/admin/settings/inputs/{StringSettingInput.js => StringSettingInput.tsx} (69%) diff --git a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.js deleted file mode 100644 index 5d9184cc42fe..000000000000 --- a/apps/meteor/client/views/admin/settings/inputs/ActionSettingInput.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Button, Field } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -function ActionSettingInput({ _id, actionText, value, disabled, sectionChanged }) { - const t = useTranslation(); - - const dispatchToastMessage = useToastMessageDispatch(); - const actionMethod = useMethod(value); - - const handleClick = async () => { - try { - const data = await actionMethod(); - const args = [data.message].concat(data.params); - dispatchToastMessage({ type: 'success', message: t(...args) }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }; - - return ( - <> - - + + {sectionChanged && {t('Save_to_enable_this_action')}} + + ); +} + +export default ActionSettingInput; diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx index b1fd40410c27..e2d56a891156 100644 --- a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.stories.tsx @@ -30,7 +30,7 @@ export const WithValue = Template.bind({}); WithValue.args = { _id: 'setting_id', label: 'Label', - value: { src: 'https://rocket.chat/images/logo.svg' }, + value: { url: 'https://rocket.chat/images/logo.svg' }, }; export const WithFileConstraints = Template.bind({}); diff --git a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx similarity index 63% rename from apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx index 2dafe8165263..009a715acccc 100644 --- a/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/AssetSettingInput.tsx @@ -1,49 +1,57 @@ import { Button, Field, Icon } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import { Random } from 'meteor/random'; -import React from 'react'; +import React, { ChangeEventHandler, DragEvent, ReactElement } from 'react'; import './AssetSettingInput.css'; -function AssetSettingInput({ _id, label, value = {}, asset, fileConstraints = {} }) { +type AssetSettingInputProps = { + _id: string; + label: string; + value?: { url: string }; + asset?: any; + fileConstraints?: { extensions: string[] }; +}; + +function AssetSettingInput({ _id, label, value, asset, fileConstraints }: AssetSettingInputProps): ReactElement { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const setAsset = useMethod('setAsset'); const unsetAsset = useMethod('unsetAsset'); - const handleUpload = (event) => { - event = event.originalEvent || event; + const isDataTransferEvent = (event: T): event is T & DragEvent => + Boolean('dataTransfer' in event && (event as any).dataTransfer.files); + const handleUpload: ChangeEventHandler = (event): void => { let { files } = event.target; + if (!files || files.length === 0) { - if (event.dataTransfer && event.dataTransfer.files) { + if (isDataTransferEvent(event)) { files = event.dataTransfer.files; - } else { - files = []; } } - Object.values(files).forEach((blob) => { + Object.values(files ?? []).forEach((blob) => { dispatchToastMessage({ type: 'info', message: t('Uploading_file') }); const reader = new FileReader(); reader.readAsBinaryString(blob); - reader.onloadend = async () => { + reader.onloadend = async (): Promise => { try { await setAsset(reader.result, blob.type, asset); dispatchToastMessage({ type: 'success', message: t('File_uploaded') }); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: String(error) }); } }; }); }; - const handleDeleteButtonClick = async () => { + const handleDeleteButtonClick = async (): Promise => { try { await unsetAsset(asset); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: String(error) }); } }; @@ -54,7 +62,7 @@ function AssetSettingInput({ _id, label, value = {}, asset, fileConstraints = {}
- {value.url ? ( + {value?.url ? (
) : (
@@ -62,7 +70,7 @@ function AssetSettingInput({ _id, label, value = {}, asset, fileConstraints = {}
)}
- {value.url ? ( + {value?.url ? (
diff --git a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx similarity index 52% rename from apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx index 003215074476..6c6a44c832eb 100644 --- a/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/BooleanSettingInput.tsx @@ -1,12 +1,31 @@ import { Field, ToggleSwitch } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { ReactElement, SyntheticEvent } from 'react'; import ResetSettingButton from '../ResetSettingButton'; -function BooleanSettingInput({ _id, label, disabled, readonly, value, hasResetButton, onChangeValue, onResetButtonClick }) { - const handleChange = (event) => { +type BooleanSettingInputProps = { + _id: string; + label: string; + disabled: boolean; + readonly: boolean; + value: boolean; + hasResetButton: boolean; + onChangeValue: (value: boolean) => void; + onResetButtonClick: () => void; +}; +function BooleanSettingInput({ + _id, + label, + disabled, + readonly, + value, + hasResetButton, + onChangeValue, + onResetButtonClick, +}: BooleanSettingInputProps): ReactElement { + const handleChange = (event: SyntheticEvent): void => { const value = event.currentTarget.checked; - onChangeValue && onChangeValue(value); + onChangeValue?.(value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror.js b/apps/meteor/client/views/admin/settings/inputs/CodeMirror.tsx similarity index 64% rename from apps/meteor/client/views/admin/settings/inputs/CodeMirror.js rename to apps/meteor/client/views/admin/settings/inputs/CodeMirror.tsx index ecd12558b7f6..500a55fc1d47 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeMirror.js +++ b/apps/meteor/client/views/admin/settings/inputs/CodeMirror.tsx @@ -1,8 +1,29 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { ReactElement, useEffect, useRef, useState } from 'react'; const defaultGutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']; +type CodeMirrorProps = { + id: string; + placeholder?: string; + disabled?: boolean; + autoComplete?: string | undefined; + lineNumbers?: boolean; + lineWrapping?: boolean; + mode?: string; + gutters?: string[]; + foldGutter?: boolean; + matchBrackets?: boolean; + autoCloseBrackets?: boolean; + matchTags?: boolean; + showTrailingSpace?: boolean; + highlightSelectionMatches?: boolean; + readOnly: boolean; + value: string; + defaultValue?: string; + onChange: (value: string) => void; +}; + function CodeMirror({ lineNumbers = true, lineWrapping = true, @@ -19,11 +40,11 @@ function CodeMirror({ defaultValue, onChange, ...props -}) { +}: CodeMirrorProps): ReactElement { const [value, setValue] = useState(valueProp || defaultValue); - const textAreaRef = useRef(); - const editorRef = useRef(); + const textAreaRef = useRef(null); + const editorRef = useRef(null); const handleChange = useMutableCallback(onChange); useEffect(() => { @@ -31,10 +52,12 @@ function CodeMirror({ return; } - const setupCodeMirror = async () => { - const CodeMirror = await import('codemirror/lib/codemirror.js'); + const setupCodeMirror = async (): Promise => { + const jsPath = 'codemirror/lib/codemirror.js'; + const CodeMirror = await import(jsPath); await import('../../../../../app/ui/client/lib/codeMirror/codeMirror'); - await import('codemirror/lib/codemirror.css'); + const cssPath = 'codemirror/lib/codemirror.css'; + await import(cssPath); if (!textAreaRef.current) { return; @@ -54,7 +77,7 @@ function CodeMirror({ readOnly, }); - editorRef.current.on('change', (doc) => { + editorRef?.current?.on('change', (doc: HTMLFormElement) => { const value = doc.getValue(); setValue(value); handleChange(value); @@ -63,7 +86,7 @@ function CodeMirror({ setupCodeMirror(); - return () => { + return (): void => { if (!editorRef.current) { return; } diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx similarity index 75% rename from apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx index 3c3b8e8ba62c..34cfdb62012f 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/CodeSettingInput.tsx @@ -1,11 +1,25 @@ import { Box, Button, Field, Flex } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import CodeMirror from './CodeMirror'; +type CodeSettingInputProps = { + _id: string; + label: string; + value?: string; + code: string; + placeholder?: string; + readonly: boolean; + autocomplete: boolean; + disabled: boolean; + hasResetButton: boolean; + onChangeValue: (value: string) => void; + onResetButtonClick: () => void; +}; + function CodeSettingInput({ _id, label, @@ -18,12 +32,12 @@ function CodeSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { +}: CodeSettingInputProps): ReactElement { const t = useTranslation(); const [fullScreen, toggleFullScreen] = useToggle(false); - const handleChange = (value) => { + const handleChange = (value: string): void => { onChangeValue(value); }; @@ -51,7 +65,7 @@ function CodeSettingInput({ onChange={handleChange} />
-
diff --git a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx similarity index 77% rename from apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx index 6d2c9f001372..31f6da2eef6b 100644 --- a/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/ColorSettingInput.tsx @@ -1,9 +1,25 @@ import { Box, Field, Flex, InputBox, Margins, TextInput, Select } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback } from 'react'; +import React, { ReactElement, useCallback } from 'react'; +import type keys from '../../../../../packages/rocketchat-i18n/i18n/en.i18n.json'; import ResetSettingButton from '../ResetSettingButton'; +type ColorSettingInputProps = { + _id: string; + label: string; + value: string; + editor: string; + allowedTypes?: (keyof typeof keys)[]; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onChangeEditor?: (value: string) => void; + onResetButtonClick?: () => void; +}; function ColorSettingInput({ _id, label, @@ -18,19 +34,19 @@ function ColorSettingInput({ onChangeValue, onChangeEditor, onResetButtonClick, -}) { +}: ColorSettingInputProps): ReactElement { const t = useTranslation(); const handleChange = useCallback( (event) => { - onChangeValue && onChangeValue(event.currentTarget.value); + onChangeValue?.(event.currentTarget.value); }, [onChangeValue], ); const handleEditorTypeChange = useCallback( (value) => { - onChangeEditor && onChangeEditor(value); + onChangeEditor?.(value); }, [onChangeEditor], ); diff --git a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx similarity index 63% rename from apps/meteor/client/views/admin/settings/inputs/FontSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx index bf93f41c2879..5df670135329 100644 --- a/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/FontSettingInput.tsx @@ -1,8 +1,20 @@ import { Box, Field, Flex, TextInput } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { FormEventHandler, ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type FontSettingInputProps = { + _id: string; + label: string; + value: string; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; function FontSettingInput({ _id, label, @@ -14,9 +26,9 @@ function FontSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - const handleChange = (event) => { - onChangeValue && onChangeValue(event.currentTarget.value); +}: FontSettingInputProps): ReactElement { + const handleChange: FormEventHandler = (event): void => { + onChangeValue?.(event.currentTarget.value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx similarity index 63% rename from apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx index d76e39a43684..974994ff6284 100644 --- a/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/GenericSettingInput.tsx @@ -1,8 +1,20 @@ import { Box, Field, Flex, TextInput } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { FormEventHandler, ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type GenericSettingInputProps = { + _id: string; + label: string; + value: string; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; function GenericSettingInput({ _id, label, @@ -14,9 +26,9 @@ function GenericSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - const handleChange = (event) => { - onChangeValue && onChangeValue(event.currentTarget.value); +}: GenericSettingInputProps): ReactElement { + const handleChange: FormEventHandler = (event): void => { + onChangeValue?.(event.currentTarget.value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx index 8c7cdd85726a..9c535e27c6eb 100644 --- a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.stories.tsx @@ -36,7 +36,7 @@ export const WithValue = Template.bind({}); WithValue.args = { _id: 'setting_id', label: 'Label', - value: 12345, + value: '12345', placeholder: 'Placeholder', }; diff --git a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx similarity index 63% rename from apps/meteor/client/views/admin/settings/inputs/IntSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx index 5c499359427c..4a5257887149 100644 --- a/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/IntSettingInput.tsx @@ -1,8 +1,21 @@ import { Box, Field, Flex, InputBox } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { FormEventHandler, ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type IntSettingInputProps = { + _id: string; + label: string; + value: string; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string | number) => void; + onResetButtonClick?: () => void; +}; + function IntSettingInput({ _id, label, @@ -14,9 +27,9 @@ function IntSettingInput({ onChangeValue, hasResetButton, onResetButtonClick, -}) { - const handleChange = (event) => { - onChangeValue && onChangeValue(parseInt(event.currentTarget.value, 10)); +}: IntSettingInputProps): ReactElement { + const handleChange: FormEventHandler = (event) => { + onChangeValue?.(parseInt(event.currentTarget.value, 10)); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx index b0d1728028f0..87b78d8aae34 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.stories.tsx @@ -45,10 +45,6 @@ WithResetButton.args = { _id: 'setting_id', label: 'Label', placeholder: 'Placeholder', - value: [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, - ], + value: ['1', '2', '3'], hasResetButton: true, }; diff --git a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx similarity index 68% rename from apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx index f6297a12dc34..850444f2a76e 100644 --- a/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/LanguageSettingInput.tsx @@ -1,9 +1,22 @@ import { Box, Field, Flex, Select } from '@rocket.chat/fuselage'; import { useLanguages } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type LanguageSettingInputProps = { + _id: string; + label: string; + value: string | number | string[]; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string | number) => void; + onResetButtonClick?: () => void; +}; + function LanguageSettingInput({ _id, label, @@ -15,11 +28,11 @@ function LanguageSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { +}: LanguageSettingInputProps): ReactElement { const languages = useLanguages(); - const handleChange = (value) => { - onChangeValue(value); + const handleChange = (value: string): void => { + onChangeValue?.(value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx index 286d8e9b2e95..02dde5261c40 100644 --- a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.stories.tsx @@ -2,7 +2,8 @@ import { Field } from '@rocket.chat/fuselage'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import MultiSelectSettingInput from './MultiSelectSettingInput'; +import type keys from '../../../../../packages/rocketchat-i18n/i18n/en.i18n.json'; +import MultiSelectSettingInput, { valuesOption } from './MultiSelectSettingInput'; export default { title: 'Admin/Settings/Inputs/MultiSelectSettingInput', @@ -17,10 +18,10 @@ export default { const Template: ComponentStory = (args) => ; -const options = [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, +const options: valuesOption[] = [ + { key: '1', i18nLabel: '1' as keyof typeof keys }, + { key: '2', i18nLabel: '2' as keyof typeof keys }, + { key: '3', i18nLabel: '3' as keyof typeof keys }, ]; export const Default = Template.bind({}); @@ -45,10 +46,7 @@ WithValue.args = { _id: 'setting_id', label: 'Label', placeholder: 'Placeholder', - value: [ - [1, 'Lorem Ipsum'], - [2, 'Lorem Ipsum'], - ], + value: ['1', 'Lorem Ipsum'], }; export const WithResetButton = Template.bind({}); diff --git a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx similarity index 65% rename from apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx index cce9e1515887..1e049a16660e 100644 --- a/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/MultiSelectSettingInput.tsx @@ -1,13 +1,29 @@ import { Field, Flex, Box, MultiSelectFiltered, MultiSelect } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; +import type keys from '../../../../../packages/rocketchat-i18n/i18n/en.i18n.json'; import ResetSettingButton from '../ResetSettingButton'; +export type valuesOption = { key: string; i18nLabel: keyof typeof keys }; +type MultiSelectSettingInputProps = { + _id: string; + label: string; + value?: [string, string]; + values: valuesOption[]; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string[]) => void; + onResetButtonClick?: () => void; +}; + function MultiSelectSettingInput({ _id, label, - value = [], + value, placeholder, readonly, disabled, @@ -16,11 +32,11 @@ function MultiSelectSettingInput({ onChangeValue, onResetButtonClick, autocomplete, -}) { +}: MultiSelectSettingInputProps): ReactElement { const t = useTranslation(); - const handleChange = (value) => { - onChangeValue && onChangeValue(value); + const handleChange = (value: string[]): void => { + onChangeValue?.(value); // onChangeValue && onChangeValue([...event.currentTarget.querySelectorAll('option')].filter((e) => e.selected).map((el) => el.value)); }; const Component = autocomplete ? MultiSelectFiltered : MultiSelect; diff --git a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx similarity index 61% rename from apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx index 43e1ba5cdaef..51f3313a2e84 100644 --- a/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/PasswordSettingInput.tsx @@ -1,8 +1,21 @@ import { Box, Field, Flex, PasswordInput } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { EventHandler, ReactElement, SyntheticEvent } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type PasswordSettingInputProps = { + _id: string; + label: string; + value?: string | number | readonly string[] | undefined; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; + function PasswordSettingInput({ _id, label, @@ -14,9 +27,9 @@ function PasswordSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - const handleChange = (event) => { - onChangeValue && onChangeValue(event.currentTarget.value); +}: PasswordSettingInputProps): ReactElement { + const handleChange: EventHandler> = (event) => { + onChangeValue?.(event.currentTarget.value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx similarity index 62% rename from apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx index 9f0c95b339bb..b7bfd80f39e6 100644 --- a/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/RelativeUrlSettingInput.tsx @@ -1,9 +1,22 @@ import { Box, Field, Flex, UrlInput } from '@rocket.chat/fuselage'; import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { EventHandler, ReactElement, SyntheticEvent } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type RelativeUrlSettingInputProps = { + _id: string; + label: string; + value?: string; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; + function RelativeUrlSettingInput({ _id, label, @@ -15,11 +28,11 @@ function RelativeUrlSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { +}: RelativeUrlSettingInputProps): ReactElement { const getAbsoluteUrl = useAbsoluteUrl(); - const handleChange = (event) => { - onChangeValue && onChangeValue(event.currentTarget.value); + const handleChange: EventHandler> = (event) => { + onChangeValue?.(event.currentTarget.value); }; return ( @@ -35,7 +48,7 @@ function RelativeUrlSettingInput({ void; + onResetButtonClick?: () => void; +}; + function RoomPickSettingInput({ _id, label, @@ -16,14 +30,12 @@ function RoomPickSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - value = value || []; - - const wrapperRef = useRef(); +}: RoomPickSettingInputProps): ReactElement { + const wrapperRef = useRef() as MutableRefObject; const valueRef = useRef(value); - const handleRemoveRoomButtonClick = (rid) => () => { - onChangeValue(value.filter(({ _id }) => _id !== rid)); + const handleRemoveRoomButtonClick = (rid: string) => (): void => { + onChangeValue?.((value || []).filter(({ _id }) => _id !== rid)); }; useLayoutEffect(() => { @@ -53,7 +65,7 @@ function RoomPickSettingInput({ template: Template.roomSearch, noMatchTemplate: Template.roomSearchEmpty, matchAll: true, - selector: (match) => ({ name: match }), + selector: (match: string): { name: string } => ({ name: match }), sort: 'name', }, ], @@ -64,12 +76,12 @@ function RoomPickSettingInput({ $('.autocomplete', wrapperRef.current).on('autocompleteselect', (event, doc) => { const { current: value } = valueRef; - onChangeValue([...value.filter(({ _id }) => _id !== doc._id), doc]); - event.currentTarget.value = ''; + onChangeValue?.([...(value || []).filter(({ _id }) => _id !== doc._id), doc]); + (event.currentTarget as HTMLInputElement).value = ''; event.currentTarget.focus(); }); - return () => { + return (): void => { Blaze.remove(view); }; }, [_id, autocomplete, disabled, onChangeValue, placeholder, readonly, valueRef]); @@ -86,7 +98,7 @@ function RoomPickSettingInput({
    - {value.map(({ _id, name }) => ( + {value?.map(({ _id, name }) => (
  • {name}
  • diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx index 7cd9cd6e14b1..4a45a5cc89ff 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.stories.tsx @@ -2,6 +2,7 @@ import { Field } from '@rocket.chat/fuselage'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; +import type keys from '../../../../../packages/rocketchat-i18n/i18n/en.i18n.json'; import SelectSettingInput from './SelectSettingInput'; export default { @@ -23,9 +24,9 @@ Default.args = { label: 'Label', placeholder: 'Placeholder', values: [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, + { key: '1', i18nLabel: '1' as keyof typeof keys }, + { key: '2', i18nLabel: '2' as keyof typeof keys }, + { key: '3', i18nLabel: '3' as keyof typeof keys }, ], }; @@ -35,9 +36,9 @@ Disabled.args = { label: 'Label', placeholder: 'Placeholder', values: [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, + { key: '1', i18nLabel: '1' as keyof typeof keys }, + { key: '2', i18nLabel: '2' as keyof typeof keys }, + { key: '3', i18nLabel: '3' as keyof typeof keys }, ], disabled: true, }; @@ -49,9 +50,9 @@ WithValue.args = { placeholder: 'Placeholder', value: '2', values: [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, + { key: '1', i18nLabel: '1' as keyof typeof keys }, + { key: '2', i18nLabel: '2' as keyof typeof keys }, + { key: '3', i18nLabel: '3' as keyof typeof keys }, ], }; @@ -61,9 +62,9 @@ WithResetButton.args = { label: 'Label', placeholder: 'Placeholder', values: [ - { key: '1', i18nLabel: '1' }, - { key: '2', i18nLabel: '2' }, - { key: '3', i18nLabel: '3' }, + { key: '1', i18nLabel: '1' as keyof typeof keys }, + { key: '2', i18nLabel: '2' as keyof typeof keys }, + { key: '3', i18nLabel: '3' as keyof typeof keys }, ], hasResetButton: true, }; diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx similarity index 64% rename from apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx index ab9f846acc84..4671a1a5e54b 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/SelectSettingInput.tsx @@ -1,9 +1,24 @@ import { Box, Field, Flex, Select } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; +import type keys from '../../../../../packages/rocketchat-i18n/i18n/en.i18n.json'; import ResetSettingButton from '../ResetSettingButton'; +type SelectSettingInputProps = { + _id: string; + label: string; + value?: string; + values?: { key: string; i18nLabel: keyof typeof keys }[]; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; + function SelectSettingInput({ _id, label, @@ -16,11 +31,11 @@ function SelectSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { +}: SelectSettingInputProps): ReactElement { const t = useTranslation(); - const handleChange = (value) => { - onChangeValue && onChangeValue(value); + const handleChange = (value: string): void => { + onChangeValue?.(value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx similarity index 68% rename from apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx index f845957b4df1..0ad6cef76b2e 100644 --- a/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/SelectTimezoneSettingInput.tsx @@ -1,9 +1,22 @@ import { Box, Field, Flex, Select } from '@rocket.chat/fuselage'; import moment from 'moment-timezone'; -import React from 'react'; +import React, { ReactElement } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type SelectTimezoneSettingInputProps = { + _id: string; + label: string; + value?: string; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; + function SelectTimezoneSettingInput({ _id, label, @@ -15,9 +28,9 @@ function SelectTimezoneSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - const handleChange = (value) => { - onChangeValue && onChangeValue(value); +}: SelectTimezoneSettingInputProps): ReactElement { + const handleChange = (value: string): void => { + onChangeValue?.(value); }; return ( diff --git a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.js b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx similarity index 69% rename from apps/meteor/client/views/admin/settings/inputs/StringSettingInput.js rename to apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx index db36f75b5d2b..a38545666fe4 100644 --- a/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.js +++ b/apps/meteor/client/views/admin/settings/inputs/StringSettingInput.tsx @@ -1,8 +1,22 @@ import { Box, Field, Flex, TextAreaInput, TextInput } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { EventHandler, ReactElement, SyntheticEvent } from 'react'; import ResetSettingButton from '../ResetSettingButton'; +type StringSettingInputProps = { + _id: string; + label: string; + value?: string; + multiline?: boolean; + placeholder?: string; + readonly?: boolean; + autocomplete?: boolean; + disabled?: boolean; + hasResetButton?: boolean; + onChangeValue?: (value: string) => void; + onResetButtonClick?: () => void; +}; + function StringSettingInput({ _id, label, @@ -15,9 +29,9 @@ function StringSettingInput({ hasResetButton, onChangeValue, onResetButtonClick, -}) { - const handleChange = (event) => { - onChangeValue(event.currentTarget.value); +}: StringSettingInputProps): ReactElement { + const handleChange: EventHandler> = (event) => { + onChangeValue?.(event.currentTarget.value); }; return ( From 72db7a246a5b4b41b0270b5598df08fcd77e1c32 Mon Sep 17 00:00:00 2001 From: souzaramon Date: Tue, 17 May 2022 09:35:07 -0300 Subject: [PATCH 4/6] Chore: Migrate 15-message-popup from cypress to playwright (#25462) --- .../meteor/tests/e2e/15-message-popup.spec.ts | 54 +++++++++++++++++++ .../e2e/utils/mocks/userAndPasswordMock.ts | 1 + .../e2e/utils/pageobjects/MainContent.ts | 7 +-- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/tests/e2e/15-message-popup.spec.ts diff --git a/apps/meteor/tests/e2e/15-message-popup.spec.ts b/apps/meteor/tests/e2e/15-message-popup.spec.ts new file mode 100644 index 000000000000..2752ddec723e --- /dev/null +++ b/apps/meteor/tests/e2e/15-message-popup.spec.ts @@ -0,0 +1,54 @@ +import { Page, test, expect } from '@playwright/test'; + +import { adminLogin } from './utils/mocks/userAndPasswordMock'; +import { userMock } from './utils/mocks/userMock'; +import LoginPage from './utils/pageobjects/LoginPage'; +import MainContent from './utils/pageobjects/MainContent'; +import SideNav from './utils/pageobjects/SideNav'; + +test.describe('[Message Popup]', () => { + let page: Page; + let loginPage: LoginPage; + let mainContent: MainContent; + let sideNav: SideNav; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + + loginPage = new LoginPage(page); + mainContent = new MainContent(page); + sideNav = new SideNav(page); + + await loginPage.goto('/'); + await loginPage.login(adminLogin); + await sideNav.openChannel('public channel'); + }); + + test.describe('User mentions', () => { + test('expect show message popup', async () => { + await mainContent.setTextToInput('@'); + expect(await mainContent.messagePopUp().isVisible()).toBeTruthy(); + }); + + test('expect popup title to be people', async () => { + await mainContent.setTextToInput('@'); + expect(await mainContent.messagePopUpTitle().locator('text=People').isVisible()).toBeTruthy(); + }); + + test('expect show "userMock.username" in options', async () => { + await mainContent.setTextToInput('@'); + expect(await mainContent.messagePopUpItems().locator(`text=${userMock.username}`).isVisible()).toBeTruthy(); + }); + + test('expect show "all" option', async () => { + await mainContent.setTextToInput('@'); + expect(await mainContent.messagePopUpItems().locator('text=all').isVisible()).toBeTruthy(); + }); + + test('expect show "here" option', async () => { + await mainContent.setTextToInput('@'); + expect(await mainContent.messagePopUpItems().locator('text=here').isVisible()).toBeTruthy(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/utils/mocks/userAndPasswordMock.ts b/apps/meteor/tests/e2e/utils/mocks/userAndPasswordMock.ts index 5ea25bf9d950..251dd75ee87a 100644 --- a/apps/meteor/tests/e2e/utils/mocks/userAndPasswordMock.ts +++ b/apps/meteor/tests/e2e/utils/mocks/userAndPasswordMock.ts @@ -19,6 +19,7 @@ export const validUserInserted: ILogin = { email: 'user.name.test@email.com', password: 'any_password', }; + const validEmail = faker.internet.email(); export const registerUser: IRegister = { diff --git a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts index 3b1be1b7a1f6..94d592393143 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts @@ -302,9 +302,10 @@ class MainContent extends BasePage { await this.messageInput().type(text); } - // Clear and sets the text to the input - public async setTextToInput(text: string): Promise { - await this.messageInput().type(text); + public async setTextToInput(text: string, options: { delay?: number } = {}): Promise { + await this.messageInput().click({ clickCount: 3 }); + await this.getPage().keyboard.press('Backspace'); + await this.messageInput().type(text, { delay: options.delay ?? 0 }); } public async dragAndDropFile(): Promise { From 88710b290c16e7e36c0e189e9750b0477feba03a Mon Sep 17 00:00:00 2001 From: souzaramon Date: Tue, 17 May 2022 10:59:56 -0300 Subject: [PATCH 5/6] Chore: Tests with Playwright (task: ROC-31, 12-settings) (#25253) --- .../client/messageBox/messageBox.html | 2 +- .../messageBox/messageBoxAudioMessage.html | 2 +- .../views/admin/users/UserInfoActions.js | 2 +- apps/meteor/package.json | 2 +- apps/meteor/tests/e2e/12-settings.spec.ts | 511 ++++++++++++++++++ apps/meteor/tests/e2e/utils/mocks/urlMock.ts | 2 + .../e2e/utils/pageobjects/Administration.ts | 18 +- .../e2e/utils/pageobjects/MainContent.ts | 23 +- .../tests/e2e/utils/pageobjects/SideNav.ts | 1 - yarn.lock | 10 +- 10 files changed, 544 insertions(+), 29 deletions(-) create mode 100644 apps/meteor/tests/e2e/12-settings.spec.ts diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index 032dbdab8afb..2bf1ffe57a03 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -35,7 +35,7 @@ {{#if canSend}} {{> messageBoxAudioMessage rid=rid tmid=tmid}} - + {{#if actions}} {{> icon block="rc-input__icon-svg" icon="plus"}} diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html index 2fb54e27ac55..806ea08ca380 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html @@ -11,7 +11,7 @@
    {{> icon block="rc-input__icon-svg" icon="checkmark-circled"}}
    -
    +
    {{> icon block="rc-input__icon-svg" icon="mic"}}
    diff --git a/apps/meteor/client/views/admin/users/UserInfoActions.js b/apps/meteor/client/views/admin/users/UserInfoActions.js index 91f086124639..c16388f7d0a0 100644 --- a/apps/meteor/client/views/admin/users/UserInfoActions.js +++ b/apps/meteor/client/views/admin/users/UserInfoActions.js @@ -318,7 +318,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, on }, [actionsDefinition, menu]); return ( - + {actions} ); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 7f3ade827db7..38c6b3013522 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -70,7 +70,7 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.14.5", "@babel/register": "^7.14.5", - "@faker-js/faker": "6.1.2", + "@faker-js/faker": "^6.3.1", "@playwright/test": "^1.21.0", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/livechat": "workspace:^", diff --git a/apps/meteor/tests/e2e/12-settings.spec.ts b/apps/meteor/tests/e2e/12-settings.spec.ts new file mode 100644 index 000000000000..dadbf331af8c --- /dev/null +++ b/apps/meteor/tests/e2e/12-settings.spec.ts @@ -0,0 +1,511 @@ +import { test, expect, Page } from '@playwright/test'; +import { v4 as uuid } from 'uuid'; + +import { BASE_API_URL } from './utils/mocks/urlMock'; +import { adminLogin, validUserInserted, registerUser } from './utils/mocks/userAndPasswordMock'; +import LoginPage from './utils/pageobjects/LoginPage'; +import MainContent from './utils/pageobjects/MainContent'; +import SideNav from './utils/pageobjects/SideNav'; +import Administration from './utils/pageobjects/Administration'; +import PreferencesMainContent from './utils/pageobjects/PreferencesMainContent'; + +const apiSessionHeaders = { 'X-Auth-Token': '', 'X-User-Id': '' }; + +test.describe('[Settings]', async () => { + let page: Page; + let loginPage: LoginPage; + let mainContent: MainContent; + let sideNav: SideNav; + let userPreferences: PreferencesMainContent; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + + loginPage = new LoginPage(page); + mainContent = new MainContent(page); + sideNav = new SideNav(page); + userPreferences = new PreferencesMainContent(page); + + await loginPage.goto('/'); + await loginPage.login(validUserInserted); + await sideNav.general().click(); + }); + + test.beforeAll(async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/login`, { data: adminLogin }); + const { userId, authToken } = (await response.json()).data; + + apiSessionHeaders['X-Auth-Token'] = authToken; + apiSessionHeaders['X-User-Id'] = userId; + }); + + test('(API) expect successfully create a session', async () => { + expect(apiSessionHeaders['X-Auth-Token'].length).toBeGreaterThan(0); + expect(apiSessionHeaders['X-User-Id'].length).toBeGreaterThan(0); + }); + + test.describe.serial('Message edit', () => { + test('(API) expect disable message editing', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowEditing`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(edit) not be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="edit-message"]')).toBeFalsy(); + }); + + test('(API) expect enable message editing', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowEditing`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(edit) be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="edit-message"]')).toBeTruthy(); + }); + }); + + test.describe.serial('Message delete', () => { + test('(API) expect disable message deleting', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowDeleting`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(delete) not be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="delete-message"]')).toBeFalsy(); + }); + + test('(API) expect enable message deleting', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowDeleting`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(delete) be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="delete-message"]')).toBeTruthy(); + }); + }); + + test.describe.serial('Audio files', () => { + test('(API) expect disable audio files', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AudioRecorderEnabled`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload audio) not be visible', async () => { + await mainContent.reload(); + + expect(await mainContent.recordBtn().isVisible()).toBeFalsy(); + }); + + test('(API) expect enable audio files', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AudioRecorderEnabled`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload audio) be visible', async () => { + await mainContent.reload(); + + expect(await mainContent.recordBtn().isVisible()).toBeTruthy(); + }); + }); + + test.describe.serial('Video files', () => { + test('(API) expect disable video files', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_VideoRecorderEnabled`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload video) not be visible', async () => { + await mainContent.reload(); + await mainContent.openMoreActionMenu(); + + expect(await page.isVisible('.rc-popover__content [data-id="video-message"]')).toBeFalsy(); + }); + + test('(API) expect enable video files', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_VideoRecorderEnabled`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload video) be visible', async () => { + await mainContent.reload(); + await mainContent.openMoreActionMenu(); + + expect(await page.isVisible('.rc-popover__content [data-id="video-message"]')).toBeTruthy(); + }); + }); + + test.describe.serial('Bad words filter', () => { + const unauthorizedWord = 'badword'; + + test('(API) expect add "badword" to filterlist', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_BadWordsFilterList`, { + headers: apiSessionHeaders, + data: { value: unauthorizedWord }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(API) expect enable bad words filter', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowBadWordsFilter`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect badword be censored', async () => { + await mainContent.reload(); + + await mainContent.sendMessage(unauthorizedWord); + await mainContent.waitForLastMessageEqualsText('*'.repeat(unauthorizedWord.length)); + }); + + test('(API) expect disable bad words filter', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowBadWordsFilter`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect badword not be censored', async () => { + await mainContent.reload(); + + await mainContent.sendMessage(unauthorizedWord); + await mainContent.waitForLastMessageEqualsText(unauthorizedWord); + }); + }); + + test.describe.serial('Message star', () => { + test('(API) expect disable message starring', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowStarring`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test.skip('(UI) expect option(star message) not be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="star-message"]')).toBeFalsy(); + }); + + test('(API) expect enable message starring', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowStarring`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(star message) be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="star-message"]')).toBeTruthy(); + }); + }); + + test.describe.serial('File upload', () => { + test('(API) expect disable file upload', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/FileUpload_Enabled`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload file) not be visible', async () => { + await mainContent.reload(); + await mainContent.openMoreActionMenu(); + + expect(await page.isVisible('[data-qa-id="file-upload"]')).toBeFalsy(); + }); + + test('(API) expect enable file upload', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/FileUpload_Enabled`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(upload file) be visible', async () => { + await mainContent.reload(); + await mainContent.openMoreActionMenu(); + + expect(await page.isVisible('[data-qa-id="file-upload"]')).toBeTruthy(); + }); + }); + + test.describe.serial('Profile change', () => { + test('(API) expect disable profile change', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_AllowUserProfileChange`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test.skip('(UI) expect options(update profile) be disabled', async () => { + await sideNav.sidebarUserMenu().click(); + await sideNav.account().click(); + + expect(userPreferences.avatarFileInput().isDisabled()).toBeTruthy(); + expect(userPreferences.emailTextInput().isDisabled()).toBeTruthy(); + expect(userPreferences.realNameTextInput().isDisabled()).toBeTruthy(); + expect(userPreferences.userNameTextInput().isDisabled()).toBeTruthy(); + }); + + test('(API) expect enable profile change', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_AllowUserProfileChange`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + }); + + test.describe.only('Avatar change', () => { + test('(API) expect disable avatar change', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_AllowUserAvatarChange`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test.skip('(UI) expect option(update avatar) be disabled', async () => { + await sideNav.sidebarUserMenu().click(); + await sideNav.account().click(); + + expect(userPreferences.avatarFileInput().isDisabled()).toBeTruthy(); + }); + + test('(API) expect enable avatar change', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_AllowUserAvatarChange`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + }); +}); + +test.describe('[Settings (admin)]', async () => { + let page: Page; + let loginPage: LoginPage; + let mainContent: MainContent; + let sideNav: SideNav; + let admin: Administration; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + + loginPage = new LoginPage(page); + mainContent = new MainContent(page); + sideNav = new SideNav(page); + admin = new Administration(page); + + await loginPage.goto('/'); + await loginPage.login(adminLogin); + await sideNav.general().click(); + }); + + test.beforeAll(async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/login`, { data: adminLogin }); + const { userId, authToken } = (await response.json()).data; + + apiSessionHeaders['X-Auth-Token'] = authToken; + apiSessionHeaders['X-User-Id'] = userId; + }); + + test.describe.serial('Message pin', () => { + test('(API) expect disable message pinning', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowPinning`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(pin message) not be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="pin-message"]')).toBeFalsy(); + }); + + test('(API) expect enable message pinning', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Message_AllowPinning`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test('(UI) expect option(pin message) be visible', async () => { + await mainContent.reload(); + await mainContent.sendMessage(`any_message_${uuid()}`); + await mainContent.openMessageActionMenu(); + + expect(await page.isVisible('[data-qa-id="pin-message"]')).toBeTruthy(); + }); + }); + + test.describe.serial('Manual new users approve', () => { + test('(API) expect enable manually approve new users', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_ManuallyApproveNewUsers`, { + headers: apiSessionHeaders, + data: { value: true }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + + test.describe('(UI) expect activate/deactivate flow as admin', () => { + test('expect open /users as admin', async () => { + await admin.goto('/admin'); + await admin.usersLink().click(); + }); + + test('expect find registered user', async () => { + await admin.usersFilter().type(registerUser.email, { delay: 200 }); + await admin.userInTable(registerUser.email).click(); + }); + + test('expect activate registered user', async () => { + await admin.userInfoActions().locator('button:nth-child(3)').click(); + await admin.getPage().locator('[value="changeActiveStatus"]').click(); + }); + + test('expect deactivate registered user', async () => { + await admin.userInfoActions().locator('button:nth-child(3)').click(); + await admin.getPage().locator('[value="changeActiveStatus"]').click(); + }); + }); + + test('(API) expect disable manually approve new users', async ({ request }) => { + const response = await request.post(`${BASE_API_URL}/settings/Accounts_ManuallyApproveNewUsers`, { + headers: apiSessionHeaders, + data: { value: false }, + }); + const data = await response.json(); + + expect(response.status()).toBe(200); + expect(data).toHaveProperty('success', true); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/utils/mocks/urlMock.ts b/apps/meteor/tests/e2e/utils/mocks/urlMock.ts index e23a083833d2..78e91b138af0 100644 --- a/apps/meteor/tests/e2e/utils/mocks/urlMock.ts +++ b/apps/meteor/tests/e2e/utils/mocks/urlMock.ts @@ -1,5 +1,7 @@ export const LOCALHOST = 'localhost:3000'; +export const BASE_API_URL = `http://${process.env.TEST_API_URL ?? LOCALHOST}/api/v1`; + export const setupWizardStepRegex = { _1: /.*\/setup-wizard\/1/, _2: /.*\/setup-wizard\/2/, diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts index 729dc4607bd9..1984cc2e9ad9 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts @@ -119,6 +119,10 @@ export default class Administration extends BasePage { return this.getPage().locator('input[placeholder="Search Users"]'); } + public userInTable(id: string): Locator { + return this.getPage().locator(`tr > td:has-text("${id}")`); + } + public rolesNewRolesButton(): Locator { return this.getPage().locator('button[aria-label="New"]'); } @@ -666,16 +670,6 @@ export default class Administration extends BasePage { public modalCancel(): Locator { return this.getPage().locator('//button[text()="Cancel"]'); } - // public async checkUserList(user: string): Promise { - // const locator = this.getPage().locator(`td=adminCreated${user}`); - - // const result = await locator.isVisible(); - // if (Array.isArray(result)) { - // return result[0]; - // } - - // return result; - // } public async publicUserFromList(user: string): Promise { await expect(this.getPage().locator(`.rcx-table__cell:first-child:contains(${user}) figure`)).toBeVisible(); @@ -688,4 +682,8 @@ export default class Administration extends BasePage { public async adminSaveChanges(): Promise { await this.buttonSave().click(); } + + public userInfoActions(): Locator { + return this.getPage().locator('[data-qa-id="UserInfoActions"]'); + } } diff --git a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts index 94d592393143..4ddfc5065821 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/MainContent.ts @@ -4,7 +4,7 @@ import { expect, Locator } from '@playwright/test'; import BasePage from './BasePage'; -class MainContent extends BasePage { +export default class MainContent extends BasePage { public mainContent(): Locator { return this.getPage().locator('.main-content'); } @@ -36,7 +36,7 @@ class MainContent extends BasePage { } public recordBtn(): Locator { - return this.getPage().locator('.js-audio-message-record'); + return this.getPage().locator('[data-qa-id="audio-record"]'); } public emojiBtn(): Locator { @@ -77,7 +77,7 @@ class MainContent extends BasePage { } public lastMessage(): Locator { - return this.getPage().locator('.message:last-child').last(); + return this.getPage().locator('.messages-box [data-qa-type="message"]').last(); } public lastMessageDesc(): Locator { @@ -131,7 +131,7 @@ class MainContent extends BasePage { } public messageActionMenu(): Locator { - return this.getPage().locator('.rc-popover .rc-popover__content'); + return this.getPage().locator('[data-qa-type="message-action-menu-options"]'); } public messageReply(): Locator { @@ -394,9 +394,6 @@ class MainContent extends BasePage { await this.emojiPickerPeopleIcon().click(); await this.emojiGrinning().click(); break; - // case 'close': - // await this.messageClose().click(); - // break; } } @@ -406,6 +403,11 @@ class MainContent extends BasePage { await this.getPage().locator('[data-qa-type="message"]:last-child div.message-actions__menu').click(); } + public async openMoreActionMenu(): Promise { + await this.getPage().locator('.rc-message-box [data-qa-id="menu-more-actions"]').click(); + await this.getPage().waitForSelector('.rc-popover__content'); + } + public async acceptDeleteMessage(): Promise { await this.modalDeleteMessageButton().click(); } @@ -437,6 +439,9 @@ class MainContent extends BasePage { public viewUserProfile(): Locator { return this.getPage().locator('[data-qa="UserCard"] a'); } -} -export default MainContent; + public async reload(): Promise { + await this.getPage().reload({ waitUntil: 'load' }); + await this.getPage().waitForSelector('.messages-box'); + } +} diff --git a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts index 195bda6856f4..b012a806ad26 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts @@ -206,7 +206,6 @@ class SideNav extends BasePage { await this.searchChannel(channelName); } - // Gets a channel from the rooms list public getChannelFromList(channelName: any): Locator { return this.getPage().locator('[data-qa="sidebar-item-title"]', { hasText: channelName }); } diff --git a/yarn.lock b/yarn.lock index 2aa4c15510e5..06d61a2bafbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1943,10 +1943,10 @@ __metadata: languageName: node linkType: hard -"@faker-js/faker@npm:6.1.2": - version: 6.1.2 - resolution: "@faker-js/faker@npm:6.1.2" - checksum: 953d0301f71e264330a2aa1e04ae4ecd8c7af5276ce932f92138923d3ef2cc69354a879a7d232805066728e6c65d1021a3f20a72b9b4c49d0ee880db9fcecada +"@faker-js/faker@npm:^6.3.1": + version: 6.3.1 + resolution: "@faker-js/faker@npm:6.3.1" + checksum: 05071e952248f4565218a95b089ef8f616130c6ff914bb416ba7c891f81d123b637c2abe8aa2eb7b207b5d4b8bd9c75c8c2133019d2d11db4176948737a95813 languageName: node linkType: hard @@ -3986,7 +3986,7 @@ __metadata: "@babel/register": ^7.14.5 "@babel/runtime": ^7.15.4 "@bugsnag/js": ^7.11.0 - "@faker-js/faker": 6.1.2 + "@faker-js/faker": ^6.3.1 "@google-cloud/language": ^3.8.0 "@google-cloud/storage": ^2.5.0 "@nivo/bar": 0.73.1 From fd40e3e6cbbe8c19b6071b51894e9cbb112ec8e8 Mon Sep 17 00:00:00 2001 From: Weslley Campos <30299972+weslley543@users.noreply.github.com> Date: Tue, 17 May 2022 13:33:19 -0300 Subject: [PATCH 6/6] Chore: migrate from cypress to pw 14-setting-permission (#25523) --- .../PermissionsTable/PermissionsTable.tsx | 14 ++- .../PermissionsTableFilter.tsx | 10 +- apps/meteor/playwright.config.ts | 3 +- apps/meteor/tests/e2e/06-messaging.spec.ts | 12 +- .../tests/e2e/14-setting-permissions.spec.ts | 116 ++++++++++++++++++ .../e2e/utils/pageobjects/Administration.ts | 20 ++- .../tests/e2e/utils/pageobjects/LoginPage.ts | 9 ++ .../tests/e2e/utils/pageobjects/SideNav.ts | 4 +- 8 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 apps/meteor/tests/e2e/14-setting-permissions.spec.ts diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx index 53a9ad7f9153..3047ad7c5528 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx @@ -57,10 +57,20 @@ const PermissionsTable = (): ReactElement => { - + {t('Permissions')} - + {t('Settings')} diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx index 824c72416b97..1f6c0f98f1ae 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx @@ -16,7 +16,15 @@ const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: stri setFilter(value); }); - return ; + return ( + + ); }; export default PermissionsTableFilter; diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index 3e821972e379..b37124707b01 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -9,12 +9,13 @@ const config: PlaywrightTestConfig = { use: { baseURL: process.env.ENTERPRISE ? 'http://localhost:4000' : 'http://localhost:3000', headless: true, - viewport: { width: 1024, height: 768 }, + viewport: { width: 1368, height: 768 }, ignoreHTTPSErrors: true, video: 'retain-on-failure', screenshot: 'only-on-failure', trace: 'retain-on-failure', }, testDir: 'tests/e2e', + retries: 3, }; export default config; diff --git a/apps/meteor/tests/e2e/06-messaging.spec.ts b/apps/meteor/tests/e2e/06-messaging.spec.ts index 50d017942c37..6be22de57680 100644 --- a/apps/meteor/tests/e2e/06-messaging.spec.ts +++ b/apps/meteor/tests/e2e/06-messaging.spec.ts @@ -64,9 +64,9 @@ test.describe('[Messaging]', () => { test.describe('[Public channel]', async () => { test.beforeAll(async ({ browser, baseURL }) => { anotherContext = await createBrowserContextForChat(browser, baseURL as string); - await anotherContext.sideNav.findFindForChat('public channel'); + await anotherContext.sideNav.findForChat('public channel'); await anotherContext.mainContent.sendMessage('Hello'); - await sideNav.findFindForChat('public channel'); + await sideNav.findForChat('public channel'); await mainContent.sendMessage('Hello'); }); test.afterAll(async () => { @@ -84,9 +84,9 @@ test.describe('[Messaging]', () => { test.describe('[Private channel]', async () => { test.beforeAll(async ({ browser, baseURL }) => { anotherContext = await createBrowserContextForChat(browser, baseURL as string); - await anotherContext.sideNav.findFindForChat('private channel'); + await anotherContext.sideNav.findForChat('private channel'); await anotherContext.mainContent.sendMessage('Hello'); - await sideNav.findFindForChat('private channel'); + await sideNav.findForChat('private channel'); await mainContent.sendMessage('Hello'); }); test.afterAll(async () => { @@ -104,9 +104,9 @@ test.describe('[Messaging]', () => { test.describe('[Direct Message]', async () => { test.beforeAll(async ({ browser, baseURL }) => { anotherContext = await createBrowserContextForChat(browser, baseURL as string); - await anotherContext.sideNav.findFindForChat('rocketchat.internal.admin.test'); + await anotherContext.sideNav.findForChat('rocketchat.internal.admin.test'); await anotherContext.mainContent.sendMessage('Hello'); - await sideNav.findFindForChat('user.name.test'); + await sideNav.findForChat('user.name.test'); await mainContent.sendMessage('Hello'); }); test.afterAll(async () => { diff --git a/apps/meteor/tests/e2e/14-setting-permissions.spec.ts b/apps/meteor/tests/e2e/14-setting-permissions.spec.ts new file mode 100644 index 000000000000..94c621b6a00a --- /dev/null +++ b/apps/meteor/tests/e2e/14-setting-permissions.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '@playwright/test'; +import faker from '@faker-js/faker'; + +import LoginPage from './utils/pageobjects/LoginPage'; +import { adminLogin, validUserInserted } from './utils/mocks/userAndPasswordMock'; +import Administration from './utils/pageobjects/Administration'; +import SideNav from './utils/pageobjects/SideNav'; + +test.describe('[Rocket.Chat Settings based permissions]', () => { + let admin: Administration; + let sideNav: SideNav; + let loginPage: LoginPage; + const newHomeTitle = faker.animal.type(); + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + const page = await context.newPage(); + sideNav = new SideNav(page); + admin = new Administration(page); + loginPage = new LoginPage(page); + }); + + test.describe('[Give User Permissions]', async () => { + test.beforeAll(async () => { + await loginPage.goto('/'); + await loginPage.login(adminLogin); + await sideNav.sidebarUserMenu().click(); + await sideNav.admin().click(); + await admin.permissionsLink().click(); + }); + + test.afterAll(async () => { + await loginPage.goto('/home'); + await loginPage.logout(); + }); + + test('Set permission for user to manage settings', async () => { + await admin.rolesSettingsFindInput().type('settings'); + await admin.getPage().locator('table tbody tr:first-child td:nth-child(1) >> text="Change some settings"').waitFor(); + const isOptionChecked = await admin.getPage().isChecked('table tbody tr:first-child td:nth-child(6) label input'); + if (!isOptionChecked) { + await admin.getPage().click('table tbody tr:first-child td:nth-child(6) label'); + } + }); + + test('Set Permission for user to change title page title', async () => { + await admin.rolesSettingsTab().click(); + await admin.rolesSettingsFindInput().fill('Layout'); + await admin.getPage().locator('table tbody tr:first-child td:nth-child(1) >> text="Layout"').waitFor(); + const isOptionChecked = await admin.getPage().isChecked('table tbody tr:first-child td:nth-child(6) label input'); + const changeHomeTitleSelected = await admin.getPage().isChecked('table tbody tr:nth-child(3) td:nth-child(6) label input'); + if (!isOptionChecked && !changeHomeTitleSelected) { + await admin.getPage().click('table tbody tr:first-child td:nth-child(6) label'); + await admin.getPage().click('table tbody tr:nth-child(3) td:nth-child(6) label'); + } + }); + }); + + test.describe('Test new user setting permissions', async () => { + test.beforeAll(async () => { + await loginPage.goto('/'); + await loginPage.login(validUserInserted); + await sideNav.sidebarUserMenu().click(); + await sideNav.admin().click(); + await admin.settingsLink().click(); + await admin.layoutSettingsButton().click(); + }); + test.afterAll(async () => { + await loginPage.goto('/home'); + await loginPage.logout(); + }); + + test('expect new permissions is enabled for user', async () => { + await admin.homeTitleInput().fill(newHomeTitle); + await admin.buttonSave().click(); + }); + }); + + test.describe('[Verify settings change and cleanup]', async () => { + test.beforeAll(async () => { + await loginPage.goto('/'); + await loginPage.login(adminLogin); + await sideNav.sidebarUserMenu().click(); + await sideNav.admin().click(); + await admin.settingsLink().click(); + await admin.settingsSearch().type('Layout'); + await admin.layoutSettingsButton().click(); + }); + + test.afterAll(async () => { + await loginPage.goto('/home'); + await loginPage.logout(); + }); + + test('New settings value visible for admin as well', async () => { + await admin.getPage().locator('[data-qa-section="Content"]').click(); + await admin.homeTitleInput().waitFor(); + const text = await admin.homeTitleInput().inputValue(); + await admin.generalHomeTitleReset().click(); + await admin.buttonSave().click(); + expect(text).toEqual(newHomeTitle); + }); + + test('Clear all user permissions', async () => { + await admin.permissionsLink().click(); + await admin.rolesSettingsFindInput().type('settings'); + await admin.getPage().locator('table tbody tr:first-child td:nth-child(1) >> text="Change some settings"').waitFor(); + await admin.getPage().click('table tbody tr:first-child td:nth-child(6) label'); + + await admin.rolesSettingsTab().click(); + await admin.rolesSettingsFindInput().fill('Layout'); + await admin.getPage().locator('table tbody tr:first-child td:nth-child(1) >> text="Layout"').waitFor(); + await admin.getPage().click('table tbody tr td:nth-child(6) label'); + await admin.getPage().click('table tbody tr:nth-child(3) td:nth-child(6) label'); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts index 1984cc2e9ad9..391bedcc9536 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts @@ -47,6 +47,10 @@ export default class Administration extends BasePage { return this.getPage().locator('[data-qa-id="General"] button'); } + public layoutSettingsButton(): Locator { + return this.getPage().locator('[data-qa-id="Layout"] button'); + } + public permissionsLink(): Locator { return this.getPage().locator('.flex-nav [href="/admin/permissions"]'); } @@ -168,15 +172,19 @@ export default class Administration extends BasePage { } public rolesSettingsFindInput(): Locator { - return this.getPage().locator('input#permissions-filter'); + return this.getPage().locator('[data-qa="PermissionTable-PermissionsTableFilter"]'); } public rolesSettingsTab(): Locator { - return this.getPage().locator('button[data-value="settings"]'); + return this.getPage().locator('[data-qa="PermissionTable-Settings"]'); } public rolesPermissionsTab(): Locator { - return this.getPage().locator('button[data-value="permissions"]'); + return this.getPage().locator('[data-qa="PermissionTable-Permissions"]'); + } + + public homeTitleInput(): Locator { + return this.getPage().locator('[data-qa-setting-id="Layout_Home_Title"]'); } // permissions grids checkboxes @@ -239,7 +247,7 @@ export default class Administration extends BasePage { // settings public buttonSave(): Locator { - return this.getPage().locator('//h2[text()="General"]/following-sibling::div//button[text()="Save changes"]'); + return this.getPage().locator('button.save'); } public generalSectionIframeIntegration(): Locator { @@ -286,6 +294,10 @@ export default class Administration extends BasePage { return this.getPage().locator('[data-qa-reset-setting-id="Site_Name"]'); } + public generalHomeTitleReset(): Locator { + return this.getPage().locator('[data-qa-reset-setting-id="Layout_Home_Title"]'); + } + public generalLanguage(): Locator { return this.getPage().locator('[data-qa-setting-id="Language"]'); } diff --git a/apps/meteor/tests/e2e/utils/pageobjects/LoginPage.ts b/apps/meteor/tests/e2e/utils/pageobjects/LoginPage.ts index 4d85de509ebc..c5472de404a7 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/LoginPage.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/LoginPage.ts @@ -81,6 +81,10 @@ class LoginPage extends BasePage { return this.getPage().locator('//span[@class="rc-header__block"]'); } + public getSideBarAvatarButton(): Locator { + return this.getPage().locator('[data-qa="sidebar-avatar-button"]'); + } + public async open(path: string): Promise { await super.goto(path); } @@ -135,6 +139,11 @@ class LoginPage extends BasePage { await expect(this.confirmPasswordInvalidText()).toBeVisible(); await expect(this.confirmPasswordInvalidText()).toHaveText('The password confirmation does not match password'); } + + public async logout(): Promise { + await this.getSideBarAvatarButton().click(); + await this.getPage().locator('li.rcx-option >> text="Logout"').click(); + } } export default LoginPage; diff --git a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts index b012a806ad26..fac4223bb61b 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/SideNav.ts @@ -240,9 +240,9 @@ class SideNav extends BasePage { // mainContent.messageInput().should('be.focused'); } - public async findFindForChat(userName: string): Promise { + public async findForChat(target: string): Promise { await this.searchUser().click(); - await this.searchInput().type(userName, { delay: 300 }); + await this.searchInput().type(target, { delay: 300 }); await this.getPage().keyboard.press(ENTER); } }