diff --git a/src/__mocks__/state-mocks.ts b/src/__mocks__/state-mocks.ts index d41641901..68278bc84 100644 --- a/src/__mocks__/state-mocks.ts +++ b/src/__mocks__/state-mocks.ts @@ -75,7 +75,7 @@ export const mockSettings: SettingsState = { participating: false, playSound: true, showNotifications: true, - showBots: true, + hideBots: false, showNotificationsCountInTray: false, openAtStartup: false, theme: Theme.SYSTEM, @@ -86,6 +86,7 @@ export const mockSettings: SettingsState = { showPills: true, keyboardShortcut: true, groupBy: GroupBy.REPOSITORY, + filterReasons: [], }; export const mockState: GitifyState = { diff --git a/src/app.tsx b/src/app.tsx index 49af30b12..843b5ccc2 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -10,6 +10,7 @@ import { Loading } from './components/Loading'; import { Sidebar } from './components/Sidebar'; import { AppContext, AppProvider } from './context/App'; import { AccountsRoute } from './routes/Accounts'; +import { FiltersRoute } from './routes/Filters'; import { LoginRoute } from './routes/Login'; import { LoginWithOAuthApp } from './routes/LoginWithOAuthApp'; import { LoginWithPersonalAccessToken } from './routes/LoginWithPersonalAccessToken'; @@ -43,6 +44,14 @@ export const App = () => { } /> + + + + } + /> { fireEvent.click(screen.getByLabelText('Go Back')); - expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith(-1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); it('should navigate back and fetch notifications', () => { @@ -43,8 +42,7 @@ describe('components/Header.tsx', () => { fireEvent.click(screen.getByLabelText('Go Back')); - expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith(-1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); expect(fetchNotifications).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 15e011cea..81e0b7b3a 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -42,7 +42,11 @@ describe('components/Sidebar.tsx', () => { it('should render itself & its children (logged out)', () => { const tree = render( @@ -55,7 +59,9 @@ describe('components/Sidebar.tsx', () => { it('should open the gitify repository', () => { render( - + @@ -74,7 +80,13 @@ describe('components/Sidebar.tsx', () => { describe('notifications icon', () => { it('when there are 0 notifications', () => { render( - + @@ -101,6 +113,7 @@ describe('components/Sidebar.tsx', () => { value={{ isLoggedIn: true, notifications: mockAccountNotifications, + settings: mockSettings, }} > @@ -132,6 +145,7 @@ describe('components/Sidebar.tsx', () => { value={{ isLoggedIn: true, notifications: mockAccountNotifications, + settings: mockSettings, }} > @@ -154,6 +168,7 @@ describe('components/Sidebar.tsx', () => { value={{ isLoggedIn: true, notifications: mockAccountNotifications, + settings: mockSettings, }} > @@ -177,6 +192,7 @@ describe('components/Sidebar.tsx', () => { value={{ isLoggedIn: true, notifications: [], + settings: mockSettings, fetchNotifications, status: 'success', }} @@ -198,6 +214,7 @@ describe('components/Sidebar.tsx', () => { value={{ isLoggedIn: true, notifications: [], + settings: mockSettings, fetchNotifications, status: 'loading', }} @@ -214,10 +231,54 @@ describe('components/Sidebar.tsx', () => { }); }); + describe('Filters', () => { + it('go to the filters route', () => { + render( + + + + + , + ); + fireEvent.click(screen.getByTitle('Filters')); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/filters'); + }); + + it('go to the home if filters path already shown', () => { + render( + + + + + , + ); + fireEvent.click(screen.getByTitle('Filters')); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/', { replace: true }); + }); + }); + describe('Settings', () => { it('go to the settings route', () => { render( - + @@ -226,13 +287,18 @@ describe('components/Sidebar.tsx', () => { fireEvent.click(screen.getByTitle('Settings')); - expect(mockNavigate).toHaveBeenCalledWith('/settings'); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/settings'); }); it('go to the home if settings path already shown', () => { render( @@ -243,15 +309,86 @@ describe('components/Sidebar.tsx', () => { fireEvent.click(screen.getByTitle('Settings')); expect(fetchNotifications).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith('/', { replace: true }); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/', { replace: true }); }); }); + it('opens github in the notifications page', () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + render( + + + + + , + ); + fireEvent.click(screen.getByLabelText('4 Unread Notifications')); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + 'https://github.com/notifications', + ); + }); + + it('opens my github issues page', () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + render( + + + + + , + ); + fireEvent.click(screen.getByLabelText('My Issues')); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + 'https://github.com/issues', + ); + }); + + it('opens my github pull requests page', () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + render( + + + + + , + ); + fireEvent.click(screen.getByLabelText('My Pull Requests')); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + 'https://github.com/pulls', + ); + }); + it('should quit the app', () => { const quitAppMock = jest.spyOn(comms, 'quitApp'); render( - + @@ -262,4 +399,68 @@ describe('components/Sidebar.tsx', () => { expect(quitAppMock).toHaveBeenCalledTimes(1); }); + + it('should open the gitify repository', () => { + const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + + render( + + + + + , + ); + fireEvent.click(screen.getByTestId('gitify-logo')); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledWith( + 'https://github.com/gitify-app/gitify', + ); + }); + + describe('should render the notifications icon', () => { + it('when there are 0 notifications', () => { + render( + + + + + , + ); + + const notificationsIcon = screen.getByTitle('0 Unread Notifications'); + expect(notificationsIcon.className).toContain('text-white'); + expect(notificationsIcon.childNodes.length).toBe(1); + expect(notificationsIcon.childNodes[0].nodeName).toBe('svg'); + }); + + it('when there are more than 0 notifications', () => { + render( + + + + + , + ); + + const notificationsIcon = screen.getByTitle('4 Unread Notifications'); + expect(notificationsIcon.className).toContain(IconColor.GREEN); + expect(notificationsIcon.childNodes.length).toBe(2); + expect(notificationsIcon.childNodes[0].nodeName).toBe('svg'); + expect(notificationsIcon.childNodes[1].nodeValue).toBe('4'); + }); + }); }); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 355c3711b..e5c007f45 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,5 +1,6 @@ import { BellIcon, + FilterIcon, GearIcon, GitPullRequestIcon, IssueOpenedIcon, @@ -11,6 +12,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { AppContext } from '../context/App'; import { Size } from '../types'; import { quitApp } from '../utils/comms'; +import { getFilterCount } from '../utils/helpers'; import { openGitHubIssues, openGitHubNotifications, @@ -25,9 +27,17 @@ export const Sidebar: FC = () => { const navigate = useNavigate(); const location = useLocation(); - const { notifications, fetchNotifications, isLoggedIn, status } = + const { notifications, fetchNotifications, isLoggedIn, status, settings } = useContext(AppContext); + const toggleFilters = () => { + if (location.pathname.startsWith('/filters')) { + navigate('/', { replace: true }); + } else { + navigate('/filters'); + } + }; + const toggleSettings = () => { if (location.pathname.startsWith('/settings')) { navigate('/', { replace: true }); @@ -46,6 +56,10 @@ export const Sidebar: FC = () => { return getNotificationCount(notifications); }, [notifications]); + const filterCount = useMemo(() => { + return getFilterCount(settings); + }, [settings]); + return (
@@ -91,6 +105,14 @@ export const Sidebar: FC = () => { onClick={() => refreshNotifications()} /> + toggleFilters()} + /> + { { label: 'Dark', value: Theme.DARK }, ]} onChange={(evt) => { - updateSetting('theme', evt.target.value); + updateSetting('theme', evt.target.value as Theme); }} /> { ); }); - it('should not be able to toggle the showBots checkbox when detailedNotifications is disabled', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - expect( - screen - .getByLabelText('Show notifications from Bot accounts') - .closest('input'), - ).toHaveProperty('disabled', true); - - // click the checkbox - fireEvent.click( - screen.getByLabelText('Show notifications from Bot accounts'), - ); - - // check if the checkbox is still unchecked - expect(updateSetting).not.toHaveBeenCalled(); - - expect( - screen.getByLabelText('Show notifications from Bot accounts').parentNode - .parentNode, - ).toMatchSnapshot(); - }); - - it('should be able to toggle the showBots checkbox when detailedNotifications is enabled', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - expect( - screen - .getByLabelText('Show notifications from Bot accounts') - .closest('input'), - ).toHaveProperty('disabled', false); - - // click the checkbox - fireEvent.click( - screen.getByLabelText('Show notifications from Bot accounts'), - ); - - // check if the checkbox is still unchecked - expect(updateSetting).toHaveBeenCalledWith('showBots', false); - - expect( - screen.getByLabelText('Show notifications from Bot accounts').parentNode - .parentNode, - ).toMatchSnapshot(); - }); - it('should toggle the markAsDoneOnOpen checkbox', async () => { await act(async () => { render( diff --git a/src/components/settings/NotificationSettings.tsx b/src/components/settings/NotificationSettings.tsx index de33b5c5f..c0880c223 100644 --- a/src/components/settings/NotificationSettings.tsx +++ b/src/components/settings/NotificationSettings.tsx @@ -22,7 +22,7 @@ export const NotificationSettings: FC = () => { { label: 'Date', value: GroupBy.DATE }, ]} onChange={(evt) => { - updateSetting('groupBy', evt.target.value); + updateSetting('groupBy', evt.target.value as GroupBy); }} /> {
} /> - - settings.detailedNotifications && - updateSetting('showBots', evt.target.checked) - } - disabled={!settings.detailedNotifications} - tooltip={ -
-
- Show or hide notifications from Bot accounts, such as @dependabot, - @renovatebot, etc -
-
- ⚠️ This setting requires Detailed Notifications to - be enabled. -
-
- } - /> -
- -
-
- -
-
-`; - -exports[`routes/components/NotificationSettings.tsx should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = ` -
-
- -
-
- -
-
-`; diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx index bc0e2d675..0a9f368a3 100644 --- a/src/context/App.test.tsx +++ b/src/context/App.test.tsx @@ -9,7 +9,7 @@ import * as comms from '../utils/comms'; import Constants from '../utils/constants'; import * as notifications from '../utils/notifications'; import * as storage from '../utils/storage'; -import { AppContext, AppProvider } from './App'; +import { AppContext, AppProvider, defaultSettings } from './App'; jest.mock('../hooks/useNotifications'); @@ -327,6 +327,14 @@ describe('context/App.tsx', () => { }); describe('settings methods', () => { + const fetchNotificationsMock = jest.fn(); + + beforeEach(() => { + (useNotifications as jest.Mock).mockReturnValue({ + fetchNotifications: fetchNotificationsMock, + }); + }); + it('should call updateSetting', async () => { const saveStateMock = jest .spyOn(storage, 'saveState') @@ -362,7 +370,7 @@ describe('context/App.tsx', () => { participating: true, playSound: true, showNotifications: true, - showBots: true, + hideBots: false, showNotificationsCountInTray: false, openAtStartup: false, theme: 'SYSTEM', @@ -373,6 +381,7 @@ describe('context/App.tsx', () => { showPills: true, keyboardShortcut: true, groupBy: 'REPOSITORY', + filterReasons: [], } as SettingsState, }); }); @@ -415,7 +424,7 @@ describe('context/App.tsx', () => { participating: false, playSound: true, showNotifications: true, - showBots: true, + hideBots: false, showNotificationsCountInTray: false, openAtStartup: true, theme: 'SYSTEM', @@ -426,8 +435,45 @@ describe('context/App.tsx', () => { showPills: true, keyboardShortcut: true, groupBy: 'REPOSITORY', + filterReasons: [], } as SettingsState, }); }); + + it('should clear filters back to default', async () => { + const saveStateMock = jest + .spyOn(storage, 'saveState') + .mockImplementation(jest.fn()); + + const TestComponent = () => { + const { clearFilters } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(saveStateMock).toHaveBeenCalledWith({ + auth: { + accounts: [], + enterpriseAccounts: [], + token: null, + user: null, + } as AuthState, + settings: { + ...mockSettings, + hideBots: defaultSettings.hideBots, + filterReasons: defaultSettings.filterReasons, + }, + }); + }); }); }); diff --git a/src/context/App.tsx b/src/context/App.tsx index 16904b58e..986ea2a28 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -15,6 +15,7 @@ import { type GitifyError, GroupBy, type SettingsState, + type SettingsValue, type Status, Theme, } from '../types'; @@ -49,11 +50,15 @@ const defaultAuth: AuthState = { user: null, }; +export const defaultFilters = { + hideBots: false, + filterReasons: [], +}; + export const defaultSettings: SettingsState = { participating: false, playSound: true, showNotifications: true, - showBots: true, showNotificationsCountInTray: false, openAtStartup: false, theme: Theme.SYSTEM, @@ -64,6 +69,7 @@ export const defaultSettings: SettingsState = { showPills: true, keyboardShortcut: true, groupBy: GroupBy.REPOSITORY, + ...defaultFilters, }; interface AppContextState { @@ -90,10 +96,8 @@ interface AppContextState { markRepoNotificationsDone: (notification: Notification) => Promise; settings: SettingsState; - updateSetting: ( - name: keyof SettingsState, - value: boolean | Theme | string | null, - ) => void; + updateSetting: (name: keyof SettingsState, value: SettingsValue) => void; + clearFilters: () => void; } export const AppContext = createContext>({}); @@ -146,7 +150,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { }, [settings.keyboardShortcut]); const updateSetting = useCallback( - (name: keyof SettingsState, value: boolean | Theme) => { + (name: keyof SettingsState, value: SettingsValue) => { if (name === 'openAtStartup') { setAutoLaunch(value as boolean); } @@ -158,6 +162,12 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { [auth, settings], ); + const clearFilters = useCallback(() => { + const newSettings = { ...settings, ...defaultFilters }; + setSettings(newSettings); + saveState({ auth, settings: newSettings }); + }, [auth]); + const isLoggedIn = useMemo(() => { return auth.accounts.length > 0; }, [auth]); @@ -290,6 +300,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { settings, updateSetting, + clearFilters, }} > {children} diff --git a/src/routes/Filters.test.tsx b/src/routes/Filters.test.tsx new file mode 100644 index 000000000..0ecb07154 --- /dev/null +++ b/src/routes/Filters.test.tsx @@ -0,0 +1,235 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { mockAuth, mockSettings } from '../__mocks__/state-mocks'; +import { AppContext } from '../context/App'; +import { FiltersRoute } from './Filters'; + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +describe('routes/Filters.tsx', () => { + const updateSetting = jest.fn(); + const clearFilters = jest.fn(); + const fetchNotifications = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('General', () => { + it('should render itself & its children', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + expect(screen.getByTestId('filters')).toMatchSnapshot(); + }); + + it('should go back by pressing the icon', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Go Back')); + expect(fetchNotifications).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); + }); + }); + describe('Users section', () => { + it('should not be able to toggle the hideBots checkbox when detailedNotifications is disabled', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + expect( + screen + .getByLabelText('Hide notifications from Bot accounts') + .closest('input'), + ).toHaveProperty('disabled', true); + + // click the checkbox + fireEvent.click( + screen.getByLabelText('Hide notifications from Bot accounts'), + ); + + // check if the checkbox is still unchecked + expect(updateSetting).not.toHaveBeenCalled(); + + expect( + screen.getByLabelText('Hide notifications from Bot accounts').parentNode + .parentNode, + ).toMatchSnapshot(); + }); + + it('should be able to toggle the hideBots checkbox when detailedNotifications is enabled', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + expect( + screen + .getByLabelText('Hide notifications from Bot accounts') + .closest('input'), + ).toHaveProperty('disabled', false); + + // click the checkbox + fireEvent.click( + screen.getByLabelText('Hide notifications from Bot accounts'), + ); + + // check if the checkbox is still unchecked + expect(updateSetting).toHaveBeenCalledWith('hideBots', true); + + expect( + screen.getByLabelText('Hide notifications from Bot accounts').parentNode + .parentNode, + ).toMatchSnapshot(); + }); + }); + + describe('Reasons section', () => { + it('should be able to toggle reason type - none already set', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + // click the checkbox + fireEvent.click(screen.getByLabelText('Mentioned')); + + // check if the checkbox is still unchecked + expect(updateSetting).toHaveBeenCalledWith('filterReasons', ['mention']); + + expect( + screen.getByLabelText('Mentioned').parentNode.parentNode, + ).toMatchSnapshot(); + }); + + it('should be able to toggle reason type - some filters already set', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + // click the checkbox + fireEvent.click(screen.getByLabelText('Mentioned')); + + // check if the checkbox is still unchecked + expect(updateSetting).toHaveBeenCalledWith('filterReasons', [ + 'security_alert', + 'mention', + ]); + + expect( + screen.getByLabelText('Mentioned').parentNode.parentNode, + ).toMatchSnapshot(); + }); + }); + + describe('Footer section', () => { + it('should clear filters', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByTitle('Clear filters')); + + expect(clearFilters).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/routes/Filters.tsx b/src/routes/Filters.tsx new file mode 100644 index 000000000..4f46f863b --- /dev/null +++ b/src/routes/Filters.tsx @@ -0,0 +1,106 @@ +import { FilterRemoveIcon } from '@primer/octicons-react'; +import { type FC, useContext } from 'react'; +import { Header } from '../components/Header'; +import { Checkbox } from '../components/fields/Checkbox'; +import { AppContext } from '../context/App'; +import { BUTTON_CLASS_NAME } from '../styles/gitify'; +import { Size } from '../types'; +import type { Reason } from '../typesGitHub'; +import { FORMATTED_REASONS, formatReason } from '../utils/reason'; + +export const FiltersRoute: FC = () => { + const { settings, clearFilters, updateSetting } = useContext(AppContext); + + const updateReasonFilter = (reason: Reason, checked: boolean) => { + let reasons: Reason[] = settings.filterReasons; + + if (checked) { + reasons.push(reason); + } else { + reasons = reasons.filter((r) => r !== reason); + } + + updateSetting('filterReasons', reasons); + }; + + const shouldShowReason = (reason: Reason) => { + return settings.filterReasons.includes(reason); + }; + + return ( +
+
Filters
+
+
+ + Users + + + settings.detailedNotifications && + updateSetting('hideBots', evt.target.checked) + } + disabled={!settings.detailedNotifications} + tooltip={ +
+
+ Hide notifications from GitHub Bot accounts, such as + @dependabot, @renovate, @netlify, etc +
+
+ ⚠️ This filter requires the{' '} + Detailed Notifications setting to be enabled. +
+
+ } + /> +
+ +
+ + Reason + + + Note: if no reasons are selected, all notifications will be shown. + + {Object.keys(FORMATTED_REASONS).map((reason: Reason) => { + return ( + + updateReasonFilter(reason, evt.target.checked) + } + tooltip={
{formatReason(reason).description}
} + /> + ); + })} +
+
+ +
+
+
+ +
+
+
+ ); +}; diff --git a/src/routes/__snapshots__/Filters.test.tsx.snap b/src/routes/__snapshots__/Filters.test.tsx.snap new file mode 100644 index 000000000..4bbea5a2c --- /dev/null +++ b/src/routes/__snapshots__/Filters.test.tsx.snap @@ -0,0 +1,1035 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`routes/Filters.tsx General should render itself & its children 1`] = ` +
+
+ +

+ Filters +

+
+
+
+ + Users + +
+
+
+ +
+
+ +
+
+
+
+
+ + Reason + + + Note: if no reasons are selected, all notifications will be shown. + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`routes/Filters.tsx Reasons section should be able to toggle reason type - none already set 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`routes/Filters.tsx Reasons section should be able to toggle reason type - some filters already set 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`routes/Filters.tsx Users section should be able to toggle the hideBots checkbox when detailedNotifications is enabled 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`routes/Filters.tsx Users section should not be able to toggle the hideBots checkbox when detailedNotifications is disabled 1`] = ` +
+
+ +
+
+ +
+
+`; diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap index d5d0b6cff..83afa9748 100644 --- a/src/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -362,54 +362,6 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = `
-
-
-
- -
-
- -
-
-
diff --git a/src/types.ts b/src/types.ts index 94aa95b8c..6fe2d25c3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type { OcticonProps } from '@primer/octicons-react'; import type { FC } from 'react'; -import type { Notification } from './typesGitHub'; +import type { Notification, Reason } from './typesGitHub'; import type { AuthMethod, EnterpriseAccount, @@ -51,9 +51,12 @@ export interface Account { user: GitifyUser | null; } +export type SettingsValue = boolean | Theme | GroupBy | Reason[]; + export type SettingsState = AppearanceSettingsState & NotificationSettingsState & - SystemSettingsState; + SystemSettingsState & + FilterSettingsState; interface AppearanceSettingsState { theme: Theme; @@ -65,7 +68,6 @@ interface AppearanceSettingsState { interface NotificationSettingsState { participating: boolean; showNotifications: boolean; - showBots: boolean; markAsDoneOnOpen: boolean; delayNotificationState: boolean; groupBy: GroupBy; @@ -78,6 +80,11 @@ interface SystemSettingsState { keyboardShortcut: boolean; } +interface FilterSettingsState { + hideBots: boolean; + filterReasons: Reason[]; +} + export interface GitifyState { auth?: AuthState; settings?: SettingsState; diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts index ab4f45a2b..2dd653150 100644 --- a/src/utils/helpers.test.ts +++ b/src/utils/helpers.test.ts @@ -1,7 +1,8 @@ import type { AxiosPromise, AxiosResponse } from 'axios'; import { mockPersonalAccessTokenAccount } from '../__mocks__/state-mocks'; -import type { Hostname, Link } from '../types'; +import { defaultSettings } from '../context/App'; +import type { Hostname, Link, SettingsState } from '../types'; import type { SubjectType } from '../typesGitHub'; import { mockGraphQLResponse, @@ -13,6 +14,7 @@ import { formatNotificationUpdatedAt, generateGitHubWebUrl, generateNotificationReferrerId, + getFilterCount, getPlatformFromHostname, isEnterpriseHost, } from './helpers'; @@ -500,4 +502,26 @@ describe('utils/helpers.ts', () => { }); }); }); + + describe('filter count', () => { + it('default filter settings', () => { + expect(getFilterCount(defaultSettings)).toBe(0); + }); + + it('non-default reason filters', () => { + const settings = { + ...defaultSettings, + filterReasons: ['subscribed', 'manual'], + } as SettingsState; + expect(getFilterCount(settings)).toBe(2); + }); + + it('non-default bot filters', () => { + const settings = { + ...defaultSettings, + hideBots: true, + } as SettingsState; + expect(getFilterCount(settings)).toBe(1); + }); + }); }); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 86ee2f5d5..cab914a54 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,6 @@ import { formatDistanceToNowStrict, parseISO } from 'date-fns'; -import type { Hostname, Link } from '../types'; +import { defaultSettings } from '../context/App'; +import type { Hostname, Link, SettingsState } from '../types'; import type { Notification } from '../typesGitHub'; import { getHtmlUrl, getLatestDiscussion } from './api/client'; import type { PlatformType } from './auth/types'; @@ -166,3 +167,20 @@ export function formatNotificationUpdatedAt( return ''; } + +export function getFilterCount(settings: SettingsState): number { + let count = 0; + + if (settings.filterReasons.length !== defaultSettings.filterReasons.length) { + count += settings.filterReasons.length; + } + + if ( + settings.detailedNotifications && + settings.hideBots !== defaultSettings.hideBots + ) { + count += 1; + } + + return count; +} diff --git a/src/utils/notifications.test.ts b/src/utils/notifications.test.ts index 5cf04ff16..28a93bf9f 100644 --- a/src/utils/notifications.test.ts +++ b/src/utils/notifications.test.ts @@ -168,24 +168,36 @@ describe('utils/notifications.ts', () => { }), ]; - it('should hide bot notifications when set to false', async () => { + it('should hide bot notifications when set to true', async () => { const result = filterNotifications(mockNotifications, { ...mockSettings, - showBots: false, + hideBots: true, }); expect(result.length).toBe(1); expect(result).toEqual([mockNotifications[0]]); }); - it('should show bot notifications when set to true', async () => { + it('should show bot notifications when set to false', async () => { const result = filterNotifications(mockNotifications, { ...mockSettings, - showBots: true, + hideBots: false, }); expect(result.length).toBe(2); expect(result).toEqual(mockNotifications); }); + + it('should filter notifications by reasons when provided', async () => { + mockNotifications[0].reason = 'subscribed'; + mockNotifications[1].reason = 'manual'; + const result = filterNotifications(mockNotifications, { + ...mockSettings, + filterReasons: ['manual'], + }); + + expect(result.length).toBe(1); + expect(result).toEqual([mockNotifications[1]]); + }); }); }); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index d65f06067..801530d3c 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -181,9 +181,17 @@ export function filterNotifications( settings: SettingsState, ): Notification[] { return notifications.filter((notification) => { - if (!settings.showBots && notification.subject?.user?.type === 'Bot') { + if (settings.hideBots && notification.subject?.user?.type === 'Bot') { return false; } + + if ( + settings.filterReasons.length > 0 && + !settings.filterReasons.includes(notification.reason) + ) { + return false; + } + return true; }); } diff --git a/src/utils/reason.ts b/src/utils/reason.ts index f8486ec0c..b14580ee1 100644 --- a/src/utils/reason.ts +++ b/src/utils/reason.ts @@ -1,7 +1,7 @@ import type { FormattedReason } from '../types'; import type { Reason } from '../typesGitHub'; -const FORMATTED_REASONS: Record = { +export const FORMATTED_REASONS: Record = { approval_requested: { title: 'Approval Requested', description: 'You were requested to review and approve a deployment.',