Skip to content

Commit c2bdc9c

Browse files
setchyafonsojramos
andauthored
feat: add user handle filters (include and exclude) (#1813)
* feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * Update src/renderer/utils/notifications/filters/userType.ts Co-authored-by: Afonso Jorge Ramos <[email protected]> * Update src/renderer/utils/notifications/filters/reason.ts Co-authored-by: Afonso Jorge Ramos <[email protected]> * Merge branch 'main' into feat/filter-by-handle Signed-off-by: Adam Setch <[email protected]> * Merge branch 'main' into feat/filter-by-handle Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> * feat: filter by handles Signed-off-by: Adam Setch <[email protected]> --------- Signed-off-by: Adam Setch <[email protected]> Co-authored-by: Afonso Jorge Ramos <[email protected]>
1 parent 6857d96 commit c2bdc9c

33 files changed

+7120
-2754
lines changed

src/renderer/__mocks__/state-mocks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ const mockSystemSettings: SystemSettingsState = {
106106
};
107107

108108
const mockFilters: FilterSettingsState = {
109-
hideBots: false,
109+
filterUserTypes: [],
110+
filterIncludeHandles: [],
111+
filterExcludeHandles: [],
110112
filterReasons: [],
111113
};
112114

src/renderer/components/AllRead.test.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ describe('renderer/components/AllRead.tsx', () => {
1414
const tree = render(
1515
<AppContext.Provider
1616
value={{
17-
settings: {
18-
...mockSettings,
19-
},
17+
settings: mockSettings,
2018
}}
2119
>
2220
<MemoryRouter>
@@ -35,7 +33,6 @@ describe('renderer/components/AllRead.tsx', () => {
3533
settings: {
3634
...mockSettings,
3735
filterReasons: ['author'],
38-
hideBots: true,
3936
},
4037
}}
4138
>

src/renderer/components/AllRead.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type FC, useContext, useMemo } from 'react';
22

33
import { AppContext } from '../context/App';
44
import { Constants } from '../utils/constants';
5-
import { hasAnyFiltersSet } from '../utils/filters';
5+
import { hasAnyFiltersSet } from '../utils/notifications/filters/filter';
66
import { EmojiSplash } from './layout/EmojiSplash';
77

88
interface IAllRead {

src/renderer/components/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import { APPLICATION } from '../../shared/constants';
1616
import { AppContext } from '../context/App';
1717
import { quitApp } from '../utils/comms';
1818
import { Constants } from '../utils/constants';
19-
import { hasAnyFiltersSet } from '../utils/filters';
2019
import {
2120
openGitHubIssues,
2221
openGitHubNotifications,
2322
openGitHubPulls,
2423
} from '../utils/links';
24+
import { hasAnyFiltersSet } from '../utils/notifications/filters/filter';
2525
import { getNotificationCount } from '../utils/notifications/notifications';
2626
import { LogoIcon } from './icons/LogoIcon';
2727

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
2+
import { MemoryRouter } from 'react-router-dom';
3+
import { mockSettings } from '../../__mocks__/state-mocks';
4+
import { AppContext } from '../../context/App';
5+
import { ReasonFilter } from './ReasonFilter';
6+
7+
describe('renderer/components/filters/ReasonFilter.tsx', () => {
8+
const updateFilter = jest.fn();
9+
10+
it('should render itself & its children', () => {
11+
const tree = render(
12+
<AppContext.Provider
13+
value={{
14+
settings: mockSettings,
15+
notifications: [],
16+
}}
17+
>
18+
<MemoryRouter>
19+
<ReasonFilter />
20+
</MemoryRouter>
21+
</AppContext.Provider>,
22+
);
23+
24+
expect(tree).toMatchSnapshot();
25+
});
26+
27+
it('should be able to toggle reason type - none already set', async () => {
28+
await act(async () => {
29+
render(
30+
<AppContext.Provider
31+
value={{
32+
settings: {
33+
...mockSettings,
34+
filterReasons: [],
35+
},
36+
notifications: [],
37+
updateFilter,
38+
}}
39+
>
40+
<MemoryRouter>
41+
<ReasonFilter />
42+
</MemoryRouter>
43+
</AppContext.Provider>,
44+
);
45+
});
46+
47+
fireEvent.click(screen.getByLabelText('Mentioned'));
48+
49+
expect(updateFilter).toHaveBeenCalledWith('filterReasons', 'mention', true);
50+
51+
expect(
52+
screen.getByLabelText('Mentioned').parentNode.parentNode,
53+
).toMatchSnapshot();
54+
});
55+
56+
it('should be able to toggle reason type - some filters already set', async () => {
57+
await act(async () => {
58+
render(
59+
<AppContext.Provider
60+
value={{
61+
settings: {
62+
...mockSettings,
63+
filterReasons: ['security_alert'],
64+
},
65+
notifications: [],
66+
updateFilter,
67+
}}
68+
>
69+
<MemoryRouter>
70+
<ReasonFilter />
71+
</MemoryRouter>
72+
</AppContext.Provider>,
73+
);
74+
});
75+
76+
fireEvent.click(screen.getByLabelText('Mentioned'));
77+
78+
expect(updateFilter).toHaveBeenCalledWith('filterReasons', 'mention', true);
79+
80+
expect(
81+
screen.getByLabelText('Mentioned').parentNode.parentNode,
82+
).toMatchSnapshot();
83+
});
84+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { type FC, useContext } from 'react';
2+
3+
import { NoteIcon } from '@primer/octicons-react';
4+
import { Stack, Text } from '@primer/react';
5+
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';
15+
16+
export const ReasonFilter: FC = () => {
17+
const { updateFilter, settings, notifications } = useContext(AppContext);
18+
19+
return (
20+
<fieldset id="filter-reasons" className="mb-3">
21+
<Title icon={NoteIcon}>Reason</Title>
22+
<Stack direction="vertical" gap="condensed">
23+
{Object.keys(FORMATTED_REASONS).map((reason: Reason) => {
24+
const reasonDetails = getReasonDetails(reason);
25+
const reasonTitle = reasonDetails.title;
26+
const reasonDescription = reasonDetails.description;
27+
const isReasonChecked = isReasonFilterSet(settings, reason);
28+
const reasonCount = getReasonFilterCount(notifications, reason);
29+
30+
return (
31+
<Checkbox
32+
key={reason}
33+
name={reasonTitle}
34+
label={reasonTitle}
35+
checked={isReasonChecked}
36+
onChange={(evt) =>
37+
updateFilter('filterReasons', reason, evt.target.checked)
38+
}
39+
tooltip={<Text>{reasonDescription}</Text>}
40+
counter={reasonCount}
41+
/>
42+
);
43+
})}
44+
</Stack>
45+
</fieldset>
46+
);
47+
};
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
2+
import { MemoryRouter } from 'react-router-dom';
3+
import { mockSettings } from '../../__mocks__/state-mocks';
4+
import { AppContext } from '../../context/App';
5+
import type { SettingsState } from '../../types';
6+
import { UserHandleFilter } from './UserHandleFilter';
7+
8+
describe('renderer/components/filters/UserHandleFilter.tsx', () => {
9+
const updateFilter = jest.fn();
10+
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
});
14+
15+
it('should render itself & its children - detailed notifications enabled', () => {
16+
const tree = render(
17+
<AppContext.Provider
18+
value={{
19+
settings: {
20+
...mockSettings,
21+
detailedNotifications: true,
22+
} as SettingsState,
23+
notifications: [],
24+
}}
25+
>
26+
<MemoryRouter>
27+
<UserHandleFilter />
28+
</MemoryRouter>
29+
</AppContext.Provider>,
30+
);
31+
32+
expect(tree).toMatchSnapshot();
33+
});
34+
35+
it('should render itself & its children - detailed notifications disabled', () => {
36+
const tree = render(
37+
<AppContext.Provider
38+
value={{
39+
settings: {
40+
...mockSettings,
41+
detailedNotifications: false,
42+
} as SettingsState,
43+
notifications: [],
44+
}}
45+
>
46+
<MemoryRouter>
47+
<UserHandleFilter />
48+
</MemoryRouter>
49+
</AppContext.Provider>,
50+
);
51+
52+
expect(tree).toMatchSnapshot();
53+
});
54+
55+
describe('Include user handles', () => {
56+
it('should be able to filter by include user handle - none already set', async () => {
57+
await act(async () => {
58+
render(
59+
<AppContext.Provider
60+
value={{
61+
settings: {
62+
...mockSettings,
63+
filterIncludeHandles: [],
64+
},
65+
notifications: [],
66+
updateFilter,
67+
}}
68+
>
69+
<MemoryRouter>
70+
<UserHandleFilter />
71+
</MemoryRouter>
72+
</AppContext.Provider>,
73+
);
74+
});
75+
76+
fireEvent.change(screen.getByTitle('Include handles'), {
77+
target: { value: 'github-user' },
78+
});
79+
80+
fireEvent.keyDown(screen.getByTitle('Include handles'), {
81+
key: 'Enter',
82+
code: 'Enter',
83+
});
84+
85+
expect(updateFilter).toHaveBeenCalledWith(
86+
'filterIncludeHandles',
87+
'github-user',
88+
true,
89+
);
90+
});
91+
92+
it('should not allow duplicate include user handle', async () => {
93+
await act(async () => {
94+
render(
95+
<AppContext.Provider
96+
value={{
97+
settings: {
98+
...mockSettings,
99+
filterIncludeHandles: ['github-user'],
100+
},
101+
notifications: [],
102+
updateFilter,
103+
}}
104+
>
105+
<MemoryRouter>
106+
<UserHandleFilter />
107+
</MemoryRouter>
108+
</AppContext.Provider>,
109+
);
110+
});
111+
112+
fireEvent.change(screen.getByTitle('Include handles'), {
113+
target: { value: 'github-user' },
114+
});
115+
116+
fireEvent.keyDown(screen.getByTitle('Include handles'), {
117+
key: 'Enter',
118+
code: 'Enter',
119+
});
120+
121+
expect(updateFilter).toHaveBeenCalledTimes(0);
122+
});
123+
});
124+
125+
describe('Exclude user handles', () => {
126+
it('should be able to filter by exclude user handle - none already set', async () => {
127+
await act(async () => {
128+
render(
129+
<AppContext.Provider
130+
value={{
131+
settings: {
132+
...mockSettings,
133+
filterExcludeHandles: [],
134+
},
135+
notifications: [],
136+
updateFilter,
137+
}}
138+
>
139+
<MemoryRouter>
140+
<UserHandleFilter />
141+
</MemoryRouter>
142+
</AppContext.Provider>,
143+
);
144+
});
145+
146+
fireEvent.change(screen.getByTitle('Exclude handles'), {
147+
target: { value: 'github-user' },
148+
});
149+
150+
fireEvent.keyDown(screen.getByTitle('Exclude handles'), {
151+
key: 'Enter',
152+
code: 'Enter',
153+
});
154+
155+
expect(updateFilter).toHaveBeenCalledWith(
156+
'filterExcludeHandles',
157+
'github-user',
158+
true,
159+
);
160+
});
161+
162+
it('should not allow duplicate exclude user handle', async () => {
163+
await act(async () => {
164+
render(
165+
<AppContext.Provider
166+
value={{
167+
settings: {
168+
...mockSettings,
169+
filterExcludeHandles: ['github-user'],
170+
},
171+
notifications: [],
172+
updateFilter,
173+
}}
174+
>
175+
<MemoryRouter>
176+
<UserHandleFilter />
177+
</MemoryRouter>
178+
</AppContext.Provider>,
179+
);
180+
});
181+
182+
fireEvent.change(screen.getByTitle('Exclude handles'), {
183+
target: { value: 'github-user' },
184+
});
185+
186+
fireEvent.keyDown(screen.getByTitle('Exclude handles'), {
187+
key: 'Enter',
188+
code: 'Enter',
189+
});
190+
191+
expect(updateFilter).toHaveBeenCalledTimes(0);
192+
});
193+
});
194+
});

0 commit comments

Comments
 (0)