Skip to content

Commit b22a6c6

Browse files
authored
refactor: reduce duplication within filter module (#1862)
* refactor: reduce duplication within filter checkbox modules Signed-off-by: Adam Setch <[email protected]> * refactor: reduce duplication within filter checkbox modules Signed-off-by: Adam Setch <[email protected]> --------- Signed-off-by: Adam Setch <[email protected]>
1 parent 488581a commit b22a6c6

30 files changed

+2563
-4772
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { MarkGithubIcon } from '@primer/octicons-react';
2+
import { act, fireEvent, render, screen } from '@testing-library/react';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import { mockAccountNotifications } from '../../__mocks__/notifications-mocks';
5+
import { mockSettings } from '../../__mocks__/state-mocks';
6+
import { AppContext } from '../../context/App';
7+
import type { SettingsState } from '../../types';
8+
import { stateFilter } from '../../utils/notifications/filters';
9+
import { FilterSection } from './FilterSection';
10+
11+
describe('renderer/components/filters/FilterSection.tsx', () => {
12+
const updateFilter = jest.fn();
13+
14+
const mockFilter = stateFilter;
15+
const mockFilterSetting = 'filterStates';
16+
17+
describe('should render itself & its children', () => {
18+
it('with detailed notifications enabled', () => {
19+
const tree = render(
20+
<AppContext.Provider
21+
value={{
22+
settings: {
23+
...mockSettings,
24+
detailedNotifications: true,
25+
} as SettingsState,
26+
notifications: mockAccountNotifications,
27+
}}
28+
>
29+
<MemoryRouter>
30+
<FilterSection
31+
id={'FilterSectionTest'}
32+
title={'FilterSectionTitle'}
33+
icon={MarkGithubIcon}
34+
filter={{
35+
...mockFilter,
36+
requiresDetailsNotifications: true,
37+
}}
38+
filterSetting={mockFilterSetting}
39+
/>
40+
</MemoryRouter>
41+
</AppContext.Provider>,
42+
);
43+
44+
expect(tree).toMatchSnapshot();
45+
});
46+
47+
it('with detailed notifications disabled', () => {
48+
const tree = render(
49+
<AppContext.Provider
50+
value={{
51+
settings: {
52+
...mockSettings,
53+
detailedNotifications: false,
54+
} as SettingsState,
55+
notifications: mockAccountNotifications,
56+
}}
57+
>
58+
<FilterSection
59+
id={'FilterSectionTest'}
60+
title={'FilterSectionTitle'}
61+
icon={MarkGithubIcon}
62+
filter={{
63+
...mockFilter,
64+
requiresDetailsNotifications: false,
65+
}}
66+
filterSetting={mockFilterSetting}
67+
/>
68+
</AppContext.Provider>,
69+
);
70+
71+
expect(tree).toMatchSnapshot();
72+
});
73+
});
74+
75+
it('should be able to toggle filter value - none already set', async () => {
76+
await act(async () => {
77+
render(
78+
<AppContext.Provider
79+
value={{
80+
settings: {
81+
...mockSettings,
82+
filterStates: [],
83+
},
84+
notifications: [],
85+
updateFilter,
86+
}}
87+
>
88+
<MemoryRouter>
89+
<FilterSection
90+
id={'FilterSectionTest'}
91+
title={'FilterSectionTitle'}
92+
icon={MarkGithubIcon}
93+
filter={mockFilter}
94+
filterSetting={mockFilterSetting}
95+
/>
96+
</MemoryRouter>
97+
</AppContext.Provider>,
98+
);
99+
});
100+
101+
fireEvent.click(screen.getByLabelText('Open'));
102+
103+
expect(updateFilter).toHaveBeenCalledWith(mockFilterSetting, 'open', true);
104+
105+
expect(
106+
screen.getByLabelText('Open').parentNode.parentNode,
107+
).toMatchSnapshot();
108+
});
109+
110+
it('should be able to toggle user type - some filters already set', async () => {
111+
await act(async () => {
112+
render(
113+
<AppContext.Provider
114+
value={{
115+
settings: {
116+
...mockSettings,
117+
filterStates: ['open'],
118+
},
119+
notifications: [],
120+
updateFilter,
121+
}}
122+
>
123+
<MemoryRouter>
124+
<FilterSection
125+
id={'FilterSectionTest'}
126+
title={'FilterSectionTitle'}
127+
icon={MarkGithubIcon}
128+
filter={mockFilter}
129+
filterSetting={mockFilterSetting}
130+
/>
131+
</MemoryRouter>
132+
</AppContext.Provider>,
133+
);
134+
});
135+
136+
fireEvent.click(screen.getByLabelText('Closed'));
137+
138+
expect(updateFilter).toHaveBeenCalledWith(
139+
mockFilterSetting,
140+
'closed',
141+
true,
142+
);
143+
144+
expect(
145+
screen.getByLabelText('Closed').parentNode.parentNode,
146+
).toMatchSnapshot();
147+
});
148+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { type ReactNode, useContext } from 'react';
2+
3+
import type { Icon } from '@primer/octicons-react';
4+
import { Stack, Text } from '@primer/react';
5+
6+
import { AppContext } from '../../context/App';
7+
import type { FilterSettingsState, FilterValue } from '../../types';
8+
import type { Filter } from '../../utils/notifications/filters';
9+
import { Checkbox } from '../fields/Checkbox';
10+
import { Tooltip } from '../fields/Tooltip';
11+
import { Title } from '../primitives/Title';
12+
import { RequiresDetailedNotificationWarning } from './RequiresDetailedNotificationsWarning';
13+
14+
export interface IFilterSection<T extends FilterValue> {
15+
id: string;
16+
title: string;
17+
icon: Icon;
18+
filter: Filter<T>;
19+
filterSetting: keyof FilterSettingsState;
20+
tooltip?: ReactNode;
21+
layout?: 'horizontal' | 'vertical';
22+
}
23+
24+
export const FilterSection = <T extends FilterValue>({
25+
id,
26+
title,
27+
icon,
28+
filter,
29+
filterSetting,
30+
tooltip,
31+
layout = 'vertical',
32+
}: IFilterSection<T>) => {
33+
const { updateFilter, settings, notifications } = useContext(AppContext);
34+
35+
return (
36+
<fieldset id={id}>
37+
<Stack direction="horizontal" gap="condensed" align="baseline">
38+
<Title icon={icon}>{title}</Title>
39+
{tooltip && (
40+
<Tooltip
41+
name={`tooltip-${id}`}
42+
tooltip={
43+
<Stack direction="vertical" gap="condensed">
44+
{tooltip}
45+
{filter.requiresDetailsNotifications && (
46+
<RequiresDetailedNotificationWarning />
47+
)}
48+
</Stack>
49+
}
50+
/>
51+
)}
52+
</Stack>
53+
54+
<Stack
55+
direction={layout}
56+
gap={layout === 'horizontal' ? 'normal' : 'condensed'}
57+
>
58+
{(Object.keys(filter.FILTER_TYPES) as T[]).map((type) => {
59+
const typeDetails = filter.getTypeDetails(type);
60+
const typeTitle = typeDetails.title;
61+
const typeDescription = typeDetails.description;
62+
const isChecked = filter.isFilterSet(settings, type);
63+
const count = filter.getFilterCount(notifications, type);
64+
65+
return (
66+
<Checkbox
67+
key={type as string}
68+
name={typeTitle}
69+
label={typeTitle}
70+
checked={isChecked}
71+
onChange={(evt) =>
72+
updateFilter(filterSetting, type, evt.target.checked)
73+
}
74+
tooltip={typeDescription ? <Text>{typeDescription}</Text> : null}
75+
disabled={
76+
filter.requiresDetailsNotifications &&
77+
!settings.detailedNotifications
78+
}
79+
counter={count}
80+
/>
81+
);
82+
})}
83+
</Stack>
84+
</fieldset>
85+
);
86+
};
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { act, fireEvent, render, screen } from '@testing-library/react';
1+
import { render } from '@testing-library/react';
22
import { MemoryRouter } from 'react-router-dom';
33
import { mockAccountNotifications } from '../../__mocks__/notifications-mocks';
44
import { mockSettings } from '../../__mocks__/state-mocks';
55
import { AppContext } from '../../context/App';
66
import { ReasonFilter } from './ReasonFilter';
77

88
describe('renderer/components/filters/ReasonFilter.tsx', () => {
9-
const updateFilter = jest.fn();
10-
119
it('should render itself & its children', () => {
1210
const tree = render(
1311
<AppContext.Provider
@@ -24,62 +22,4 @@ describe('renderer/components/filters/ReasonFilter.tsx', () => {
2422

2523
expect(tree).toMatchSnapshot();
2624
});
27-
28-
it('should be able to toggle reason type - none already set', async () => {
29-
await act(async () => {
30-
render(
31-
<AppContext.Provider
32-
value={{
33-
settings: {
34-
...mockSettings,
35-
filterReasons: [],
36-
},
37-
notifications: [],
38-
updateFilter,
39-
}}
40-
>
41-
<MemoryRouter>
42-
<ReasonFilter />
43-
</MemoryRouter>
44-
</AppContext.Provider>,
45-
);
46-
});
47-
48-
fireEvent.click(screen.getByLabelText('Mentioned'));
49-
50-
expect(updateFilter).toHaveBeenCalledWith('filterReasons', 'mention', true);
51-
52-
expect(
53-
screen.getByLabelText('Mentioned').parentNode.parentNode,
54-
).toMatchSnapshot();
55-
});
56-
57-
it('should be able to toggle reason type - some filters already set', async () => {
58-
await act(async () => {
59-
render(
60-
<AppContext.Provider
61-
value={{
62-
settings: {
63-
...mockSettings,
64-
filterReasons: ['security_alert'],
65-
},
66-
notifications: [],
67-
updateFilter,
68-
}}
69-
>
70-
<MemoryRouter>
71-
<ReasonFilter />
72-
</MemoryRouter>
73-
</AppContext.Provider>,
74-
);
75-
});
76-
77-
fireEvent.click(screen.getByLabelText('Mentioned'));
78-
79-
expect(updateFilter).toHaveBeenCalledWith('filterReasons', 'mention', true);
80-
81-
expect(
82-
screen.getByLabelText('Mentioned').parentNode.parentNode,
83-
).toMatchSnapshot();
84-
});
8525
});
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,20 @@
1-
import { type FC, useContext } from 'react';
1+
import type { FC } from 'react';
22

33
import { NoteIcon } from '@primer/octicons-react';
4-
import { Stack, Text } from '@primer/react';
4+
import { Text } from '@primer/react';
55

6-
import { AppContext } from '../../context/App';
7-
import type { Reason } from '../../typesGitHub';
8-
import {
9-
getReasonFilterCount,
10-
isReasonFilterSet,
11-
} from '../../utils/notifications/filters/reason';
12-
import { FORMATTED_REASONS, getReasonDetails } from '../../utils/reason';
13-
import { Checkbox } from '../fields/Checkbox';
14-
import { Title } from '../primitives/Title';
6+
import { reasonFilter } from '../../utils/notifications/filters';
7+
import { FilterSection } from './FilterSection';
158

169
export const ReasonFilter: FC = () => {
17-
const { updateFilter, settings, notifications } = useContext(AppContext);
18-
1910
return (
20-
<fieldset id="filter-reasons">
21-
<Title icon={NoteIcon}>Reason</Title>
22-
23-
<Stack direction="vertical" gap="condensed">
24-
{Object.keys(FORMATTED_REASONS).map((reason: Reason) => {
25-
const reasonDetails = getReasonDetails(reason);
26-
const reasonTitle = reasonDetails.title;
27-
const reasonDescription = reasonDetails.description;
28-
const isReasonChecked = isReasonFilterSet(settings, reason);
29-
const reasonCount = getReasonFilterCount(notifications, reason);
30-
31-
return (
32-
<Checkbox
33-
key={reason}
34-
name={reasonTitle}
35-
label={reasonTitle}
36-
checked={isReasonChecked}
37-
onChange={(evt) =>
38-
updateFilter('filterReasons', reason, evt.target.checked)
39-
}
40-
tooltip={<Text>{reasonDescription}</Text>}
41-
counter={reasonCount}
42-
/>
43-
);
44-
})}
45-
</Stack>
46-
</fieldset>
11+
<FilterSection
12+
id="filter-reasons"
13+
title="Reason"
14+
icon={NoteIcon}
15+
filter={reasonFilter}
16+
filterSetting="filterReasons"
17+
tooltip={<Text>Filter notifications by reason.</Text>}
18+
/>
4719
);
4820
};

0 commit comments

Comments
 (0)