Skip to content

Commit 9d4b2b8

Browse files
wip
1 parent 4aba7c8 commit 9d4b2b8

File tree

7 files changed

+176
-33
lines changed

7 files changed

+176
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { OptionProp } from '@rocket.chat/ui-client';
2+
import type { Dispatch, SetStateAction } from 'react';
3+
import { createContext } from 'react';
4+
5+
export type SearchFilters = {
6+
searchText: string;
7+
types: OptionProp[];
8+
};
9+
10+
type SearchFilterContextValue = {
11+
searchFilters: SearchFilters;
12+
setSearchFilters: Dispatch<SetStateAction<SearchFilters>>;
13+
};
14+
15+
export const SearchFilterContext = createContext<SearchFilterContextValue>({
16+
searchFilters: { searchText: '', types: [] },
17+
setSearchFilters: () => undefined,
18+
});

apps/meteor/client/providers/MeteorProvider.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import LayoutProvider from './LayoutProvider';
1515
import ModalProvider from './ModalProvider/ModalProvider';
1616
import OmnichannelProvider from './OmnichannelProvider';
1717
import RouterProvider from './RouterProvider';
18+
import { SearchFilterProvider } from './SearchFiltersProvider/SearchFilterProvider';
1819
import ServerProvider from './ServerProvider';
1920
import SessionProvider from './SessionProvider';
2021
import SettingsProvider from './SettingsProvider';
@@ -48,7 +49,9 @@ const MeteorProvider: FC = ({ children }) => (
4849
<ActionManagerProvider>
4950
<VideoConfProvider>
5051
<CallProvider>
51-
<OmnichannelProvider>{children}</OmnichannelProvider>
52+
<OmnichannelProvider>
53+
<SearchFilterProvider>{children}</SearchFilterProvider>
54+
</OmnichannelProvider>
5255
</CallProvider>
5356
</VideoConfProvider>
5457
</ActionManagerProvider>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* eslint-disable no-await-in-loop */
2+
import '@testing-library/jest-dom';
3+
import type { OptionProp } from '@rocket.chat/ui-client';
4+
import { render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event';
6+
import React, { useContext } from 'react';
7+
8+
import type { SearchFilters } from '../../contexts/SearchFilterContext';
9+
import { SearchFilterContext } from '../../contexts/SearchFilterContext';
10+
import { SearchFilterProvider } from './SearchFilterProvider';
11+
12+
const DropdownTestComponent = () => {
13+
const { searchFilters, setSearchFilters } = useContext(SearchFilterContext);
14+
15+
const handleCheckboxChange = (id: string) => {
16+
setSearchFilters({
17+
...searchFilters,
18+
types: searchFilters.types.map((type) => (type.id === id ? { ...type, checked: !type.checked } : type)),
19+
} as SearchFilters);
20+
};
21+
22+
const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
23+
setSearchFilters({ ...searchFilters, searchText: e.target.value });
24+
};
25+
26+
return (
27+
<div>
28+
<input type='text' value={searchFilters.searchText} onChange={handleSearchTextChange} placeholder='Search...' aria-label='Search' />
29+
{searchFilters.types.map((option) =>
30+
option.isGroupTitle ? (
31+
<div key={option.id}>{option.text}</div>
32+
) : (
33+
<label key={option.id}>
34+
<input type='checkbox' checked={option.checked} onChange={() => handleCheckboxChange(option.id)} />
35+
{option.text}
36+
</label>
37+
),
38+
)}
39+
</div>
40+
);
41+
};
42+
43+
describe('SearchFilterProvider', () => {
44+
test('handles types in context correctly', async () => {
45+
const initialTypes = [
46+
{ id: '1', text: 'Type 1', isGroupTitle: false, checked: false },
47+
{ id: '2', text: 'Type 2', isGroupTitle: false, checked: false },
48+
] as OptionProp[];
49+
50+
render(
51+
<SearchFilterProvider initialState={{ searchText: '', types: initialTypes }}>
52+
<DropdownTestComponent />
53+
</SearchFilterProvider>,
54+
);
55+
56+
for (const type of initialTypes) {
57+
const checkbox = await screen.findByLabelText(type.text);
58+
expect(checkbox).not.toBeChecked();
59+
}
60+
61+
const firstCheckbox = await screen.findByLabelText(initialTypes[0].text);
62+
userEvent.click(firstCheckbox);
63+
64+
const updatedFirstCheckbox = await screen.findByLabelText(initialTypes[0].text);
65+
expect(updatedFirstCheckbox).toBeChecked();
66+
});
67+
68+
test('handles searchText and types in context correctly', async () => {
69+
const initialTypes: OptionProp[] = [
70+
{ id: '1', text: 'Type 1', isGroupTitle: false, checked: false },
71+
{ id: '2', text: 'Type 2', isGroupTitle: false, checked: false },
72+
] as OptionProp[];
73+
74+
render(
75+
<SearchFilterProvider initialState={{ searchText: '', types: initialTypes }}>
76+
<DropdownTestComponent />
77+
</SearchFilterProvider>,
78+
);
79+
80+
const searchInput = screen.getByLabelText('Search');
81+
expect(searchInput).toHaveValue('');
82+
83+
userEvent.type(searchInput, 'New Search Text');
84+
85+
expect(searchInput).toHaveValue('New Search Text');
86+
});
87+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { FC, ReactNode } from 'react';
2+
import React, { useState } from 'react';
3+
4+
import type { SearchFilters } from '../../contexts/SearchFilterContext';
5+
import { SearchFilterContext } from '../../contexts/SearchFilterContext';
6+
7+
type SearchFilterProviderProps = {
8+
children: ReactNode;
9+
initialState?: SearchFilters;
10+
};
11+
12+
export const SearchFilterProvider: FC<SearchFilterProviderProps> = ({ children, initialState }) => {
13+
const [searchFilters, setSearchFilters] = useState<SearchFilters>(initialState || { searchText: '', types: [] });
14+
15+
return <SearchFilterContext.Provider value={{ searchFilters, setSearchFilters }}>{children}</SearchFilterContext.Provider>;
16+
};

apps/meteor/client/views/admin/rooms/RoomsTable.tsx

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage';
22
import { useMediaQuery, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
3-
import type { OptionProp } from '@rocket.chat/ui-client';
43
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
54
import { useQuery } from '@tanstack/react-query';
65
import type { ReactElement, MutableRefObject } from 'react';
7-
import React, { useRef, useState, useEffect, useMemo } from 'react';
6+
import React, { useRef, useEffect, useMemo, useContext } from 'react';
87

98
import GenericNoResults from '../../../components/GenericNoResults';
109
import {
@@ -16,41 +15,43 @@ import {
1615
} from '../../../components/GenericTable';
1716
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
1817
import { useSort } from '../../../components/GenericTable/hooks/useSort';
18+
import type { SearchFilters } from '../../../contexts/SearchFilterContext';
19+
import { SearchFilterContext } from '../../../contexts/SearchFilterContext';
1920
import RoomRow from './RoomRow';
2021
import RoomsTableFilters from './RoomsTableFilters';
2122

22-
type RoomFilters = {
23-
searchText: string;
24-
types: OptionProp[];
25-
};
26-
2723
const DEFAULT_TYPES = ['d', 'p', 'c', 'l', 'discussions', 'teams'];
2824

2925
const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement => {
3026
const t = useTranslation();
3127
const mediaQuery = useMediaQuery('(min-width: 1024px)');
3228

33-
const [roomFilters, setRoomFilters] = useState<RoomFilters>({ searchText: '', types: [] });
29+
const { searchFilters } = useContext(SearchFilterContext);
3430

35-
const prevRoomFilterText = useRef<string>(roomFilters.searchText);
31+
const prevRoomFilters = useRef<SearchFilters>(searchFilters);
3632

3733
const { sortBy, sortDirection, setSort } = useSort<'name' | 't' | 'usersCount' | 'msgs' | 'default' | 'featured'>('name');
3834
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
39-
const searchText = useDebouncedValue(roomFilters.searchText, 500);
35+
const searchText = useDebouncedValue(searchFilters.searchText, 500);
4036

4137
const query = useDebouncedValue(
4238
useMemo(() => {
43-
if (searchText !== prevRoomFilterText.current) {
39+
const filtersChanged =
40+
searchText !== prevRoomFilters.current.searchText ||
41+
JSON.stringify(searchFilters.types) !== JSON.stringify(prevRoomFilters.current.types);
42+
43+
if (filtersChanged) {
4444
setCurrent(0);
4545
}
46+
4647
return {
4748
filter: searchText || '',
4849
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
4950
count: itemsPerPage,
50-
offset: searchText === prevRoomFilterText.current ? current : 0,
51-
types: roomFilters.types.length ? [...roomFilters.types.map((roomType) => roomType.id)] : DEFAULT_TYPES,
51+
offset: filtersChanged ? 0 : current,
52+
types: searchFilters.types.length ? [...searchFilters.types.map((roomType) => roomType.id)] : DEFAULT_TYPES,
5253
};
53-
}, [searchText, sortBy, sortDirection, itemsPerPage, current, roomFilters.types, setCurrent]),
54+
}, [searchText, sortBy, sortDirection, itemsPerPage, current, searchFilters.types, setCurrent]),
5455
500,
5556
);
5657

@@ -63,8 +64,8 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React
6364
}, [reload, refetch]);
6465

6566
useEffect(() => {
66-
prevRoomFilterText.current = searchText;
67-
}, [searchText]);
67+
prevRoomFilters.current = searchFilters;
68+
}, [searchFilters]);
6869

6970
const headers = (
7071
<>
@@ -116,7 +117,7 @@ const RoomsTable = ({ reload }: { reload: MutableRefObject<() => void> }): React
116117

117118
return (
118119
<>
119-
<RoomsTableFilters setFilters={setRoomFilters} />
120+
<RoomsTableFilters />
120121
{isLoading && (
121122
<GenericTable>
122123
<GenericTableHeader>{headers}</GenericTableHeader>

apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx

+13-15
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { Box, Icon, TextInput } from '@rocket.chat/fuselage';
22
import type { OptionProp } from '@rocket.chat/ui-client';
33
import { MultiSelectCustom } from '@rocket.chat/ui-client';
44
import { useTranslation } from '@rocket.chat/ui-contexts';
5-
import React, { useCallback, useState } from 'react';
5+
import React, { useCallback, useContext } from 'react';
66
import type { Dispatch, ReactElement, SetStateAction } from 'react';
77

8+
import { SearchFilterContext } from '../../../contexts/SearchFilterContext';
9+
810
const roomTypeFilterStructure = [
911
{
1012
id: 'filter_by_room',
@@ -43,27 +45,23 @@ const roomTypeFilterStructure = [
4345
},
4446
] as OptionProp[];
4547

46-
const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch<SetStateAction<any>> }): ReactElement => {
48+
const RoomsTableFilters = (): ReactElement => {
49+
const { searchFilters, setSearchFilters } = useContext(SearchFilterContext);
4750
const t = useTranslation();
48-
const [text, setText] = useState('');
49-
50-
const [roomTypeSelectedOptions, setRoomTypeSelectedOptions] = useState<OptionProp[]>([]);
5151

5252
const handleSearchTextChange = useCallback(
53-
(event) => {
54-
const text = event.currentTarget.value;
55-
setFilters({ searchText: text, types: roomTypeSelectedOptions });
56-
setText(text);
53+
(event: React.ChangeEvent<HTMLInputElement>) => {
54+
const newText = event.currentTarget.value;
55+
setSearchFilters((filters) => ({ ...filters, searchText: newText, types: searchFilters.types }));
5756
},
58-
[roomTypeSelectedOptions, setFilters],
57+
[searchFilters.types, setSearchFilters],
5958
);
6059

6160
const handleRoomTypeChange = useCallback(
6261
(options: OptionProp[]) => {
63-
setFilters({ searchText: text, types: options });
64-
setRoomTypeSelectedOptions(options);
62+
setSearchFilters((filters) => ({ ...filters, types: options, searchText: searchFilters.searchText }));
6563
},
66-
[text, setFilters],
64+
[searchFilters.searchText, setSearchFilters],
6765
) as Dispatch<SetStateAction<OptionProp[]>>;
6866

6967
return (
@@ -83,7 +81,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch<SetStateAction
8381
placeholder={t('Search_rooms')}
8482
addon={<Icon name='magnifier' size='x20' />}
8583
onChange={handleSearchTextChange}
86-
value={text}
84+
value={searchFilters.searchText}
8785
/>
8886
</Box>
8987
<Box minWidth='x224' m='x4'>
@@ -92,7 +90,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch<SetStateAction
9290
defaultTitle={'All_rooms' as any}
9391
selectedOptionsTitle='Rooms'
9492
setSelectedOptions={handleRoomTypeChange}
95-
selectedOptions={roomTypeSelectedOptions}
93+
selectedOptions={searchFilters.types}
9694
/>
9795
</Box>
9896
</Box>
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Page } from '@playwright/test';
2+
3+
import { Users } from './fixtures/userStates';
4+
import { expect, test } from './utils/test';
5+
6+
test.use({ storageState: Users.admin.state });
7+
8+
test.describe.serial('admin-rooms', () => {
9+
let adminPage: Page;
10+
test.beforeAll(async ({ browser }) => {
11+
adminPage = await browser.newPage({ storageState: Users.admin.state });
12+
await adminPage.goto('/home');
13+
await adminPage.waitForSelector('[data-qa-id="home-header"]');
14+
});
15+
test('should display the Rooms Table', async ({ page }) => {
16+
await page.goto('/admin/rooms');
17+
18+
await expect(page.locator('text=Rooms Table')).toBeVisible();
19+
});
20+
});

0 commit comments

Comments
 (0)