diff --git a/src/components/settings/AppearanceSettings.test.tsx b/src/components/settings/AppearanceSettings.test.tsx
new file mode 100644
index 000000000..96cae84b7
--- /dev/null
+++ b/src/components/settings/AppearanceSettings.test.tsx
@@ -0,0 +1,111 @@
+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 { AppearanceSettings } from './AppearanceSettings';
+
+describe('routes/components/AppearanceSettings.tsx', () => {
+ const updateSetting = jest.fn();
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should change the theme radio group', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Light'));
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
+ });
+
+ it('should toggle detailed notifications checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ await screen.findByLabelText('Detailed notifications');
+
+ fireEvent.click(screen.getByLabelText('Detailed notifications'));
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('detailedNotifications', false);
+ });
+
+ it('should toggle metric pills checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ await screen.findByLabelText('Show notification metric pills');
+
+ fireEvent.click(screen.getByLabelText('Show notification metric pills'));
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('showPills', false);
+ });
+
+ it('should toggle account hostname checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ await screen.findByLabelText('Show account hostname');
+
+ fireEvent.click(screen.getByLabelText('Show account hostname'));
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('showAccountHostname', true);
+ });
+});
diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx
new file mode 100644
index 000000000..e50a818ae
--- /dev/null
+++ b/src/components/settings/AppearanceSettings.tsx
@@ -0,0 +1,111 @@
+import {
+ CheckIcon,
+ CommentIcon,
+ IssueClosedIcon,
+ MilestoneIcon,
+ TagIcon,
+} from '@primer/octicons-react';
+import { ipcRenderer } from 'electron';
+import { type FC, useContext, useEffect } from 'react';
+import { AppContext } from '../../context/App';
+import { Size, Theme } from '../../types';
+import { setTheme } from '../../utils/theme';
+import { Checkbox } from '../fields/Checkbox';
+import { RadioGroup } from '../fields/RadioGroup';
+
+export const AppearanceSettings: FC = () => {
+ const { settings, updateSetting } = useContext(AppContext);
+
+ useEffect(() => {
+ ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => {
+ if (settings.theme === Theme.SYSTEM) {
+ setTheme(updatedTheme);
+ }
+ });
+ }, [settings.theme]);
+
+ return (
+
+
+ Appearance
+
+ {
+ updateSetting('theme', evt.target.value);
+ }}
+ />
+
+ updateSetting('detailedNotifications', evt.target.checked)
+ }
+ tooltip={
+
+
+ Enrich notifications with author or last commenter profile
+ information, state and GitHub-like colors.
+
+
+ ⚠️ Users with a large number of unread notifications may {' '}
+ experience rate limiting under certain circumstances. Disable this
+ setting if you experience this.
+
+
+ }
+ />
+ updateSetting('showPills', evt.target.checked)}
+ tooltip={
+
+
Show notification metric pills for:
+
+
+
+
+ linked issues
+
+
+ pr reviews
+
+
+
+ comments
+
+
+
+
+ labels
+
+
+
+ milestones
+
+
+
+
+ }
+ />
+
+ updateSetting('showAccountHostname', evt.target.checked)
+ }
+ />
+
+ );
+};
diff --git a/src/components/settings/NotificationSettings.test.tsx b/src/components/settings/NotificationSettings.test.tsx
new file mode 100644
index 000000000..b6ea137c6
--- /dev/null
+++ b/src/components/settings/NotificationSettings.test.tsx
@@ -0,0 +1,230 @@
+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 * as comms from '../../utils/comms';
+import { NotificationSettings } from './NotificationSettings';
+
+describe('routes/components/NotificationSettings.tsx', () => {
+ const updateSetting = jest.fn();
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should change the groupBy radio group', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Date'));
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('groupBy', 'DATE');
+ });
+ it('should toggle the showOnlyParticipating checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Show only participating'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('participating', false);
+ });
+
+ it('should open official docs for showOnlyParticipating tooltip', async () => {
+ const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ const tooltipElement = screen.getByLabelText(
+ 'tooltip-showOnlyParticipating',
+ );
+
+ fireEvent.mouseEnter(tooltipElement);
+
+ fireEvent.click(
+ screen.getByTitle(
+ 'Open GitHub documentation for participating and watching notifications',
+ ),
+ );
+
+ expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
+ expect(openExternalLinkMock).toHaveBeenCalledWith(
+ 'https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#about-participating-and-watching-notifications',
+ );
+ });
+
+ 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(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Mark as done on open'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false);
+ });
+
+ it('should toggle the delayNotificationState checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Delay notification state'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('delayNotificationState', false);
+ });
+});
diff --git a/src/components/settings/NotificationSettings.tsx b/src/components/settings/NotificationSettings.tsx
new file mode 100644
index 000000000..de33b5c5f
--- /dev/null
+++ b/src/components/settings/NotificationSettings.tsx
@@ -0,0 +1,99 @@
+import { type FC, type MouseEvent, useContext } from 'react';
+import { AppContext } from '../../context/App';
+import { GroupBy } from '../../types';
+import { openGitHubParticipatingDocs } from '../../utils/links';
+import { Checkbox } from '../fields/Checkbox';
+import { RadioGroup } from '../fields/RadioGroup';
+
+export const NotificationSettings: FC = () => {
+ const { settings, updateSetting } = useContext(AppContext);
+
+ return (
+
+
+ Notifications
+
+ {
+ updateSetting('groupBy', evt.target.value);
+ }}
+ />
+ updateSetting('participating', evt.target.checked)}
+ tooltip={
+
+ See
+ ) => {
+ // Don't trigger onClick of parent element.
+ event.stopPropagation();
+ openGitHubParticipatingDocs();
+ }}
+ >
+ official docs
+
+ for more details.
+
+ }
+ />
+
+ 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.
+
+
+ }
+ />
+
+ updateSetting('markAsDoneOnOpen', evt.target.checked)
+ }
+ />
+
+ updateSetting('delayNotificationState', evt.target.checked)
+ }
+ tooltip={
+
+ Keep the notification within Gitify window upon interaction (click,
+ mark as read, mark as done, etc) until the next refresh window
+ (scheduled or user initiated).
+
+ }
+ />
+
+ );
+};
diff --git a/src/components/settings/SettingsFooter.test.tsx b/src/components/settings/SettingsFooter.test.tsx
new file mode 100644
index 000000000..546c75997
--- /dev/null
+++ b/src/components/settings/SettingsFooter.test.tsx
@@ -0,0 +1,148 @@
+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 * as comms from '../../utils/comms';
+import { SettingsFooter } from './SettingsFooter';
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: () => mockNavigate,
+}));
+
+describe('routes/components/SettingsFooter.tsx', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('app version', () => {
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeEach(() => {
+ // Save the original node env state
+ originalEnv = process.env;
+ });
+
+ afterEach(() => {
+ // Restore the original node env state
+ process.env = originalEnv;
+ });
+
+ it('should show production app version', async () => {
+ process.env = {
+ ...originalEnv,
+ NODE_ENV: 'production',
+ };
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(screen.getByTitle('app-version')).toMatchSnapshot();
+ });
+
+ it('should show development app version', async () => {
+ process.env = {
+ ...originalEnv,
+ NODE_ENV: 'development',
+ };
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(screen.getByTitle('app-version')).toMatchSnapshot();
+ });
+ });
+
+ it('should open release notes', async () => {
+ const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByTitle('View release notes'));
+
+ expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
+ expect(openExternalLinkMock).toHaveBeenCalledWith(
+ 'https://github.com/gitify-app/gitify/releases/tag/v0.0.1',
+ );
+ });
+
+ it('should open account management', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByTitle('Accounts'));
+ expect(mockNavigate).toHaveBeenCalledWith('/accounts');
+ });
+
+ it('should quit the app', async () => {
+ const quitAppMock = jest.spyOn(comms, 'quitApp');
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByTitle('Quit Gitify'));
+ expect(quitAppMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/settings/SettingsFooter.tsx b/src/components/settings/SettingsFooter.tsx
new file mode 100644
index 000000000..54d4c4708
--- /dev/null
+++ b/src/components/settings/SettingsFooter.tsx
@@ -0,0 +1,57 @@
+import { PersonIcon, XCircleIcon } from '@primer/octicons-react';
+import { type FC, useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { BUTTON_CLASS_NAME } from '../../styles/gitify';
+import { Size } from '../../types';
+import { getAppVersion, quitApp } from '../../utils/comms';
+import { openGitifyReleaseNotes } from '../../utils/links';
+
+export const SettingsFooter: FC = () => {
+ const [appVersion, setAppVersion] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ (async () => {
+ if (process.env.NODE_ENV === 'development') {
+ setAppVersion('dev');
+ } else {
+ const result = await getAppVersion();
+ setAppVersion(`v${result}`);
+ }
+ })();
+ }, []);
+
+ return (
+
+
openGitifyReleaseNotes(appVersion)}
+ >
+ Gitify {appVersion}
+
+
+
{
+ navigate('/accounts');
+ }}
+ >
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/settings/SystemSettings.test.tsx b/src/components/settings/SystemSettings.test.tsx
new file mode 100644
index 000000000..3ca7c6df0
--- /dev/null
+++ b/src/components/settings/SystemSettings.test.tsx
@@ -0,0 +1,141 @@
+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 { SystemSettings } from './SystemSettings';
+
+describe('routes/components/SystemSettings.tsx', () => {
+ const updateSetting = jest.fn();
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should toggle the keyboardShortcut checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Enable keyboard shortcut'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('keyboardShortcut', false);
+ });
+
+ it('should toggle the showNotificationsCountInTray checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Show notifications count in tray'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith(
+ 'showNotificationsCountInTray',
+ false,
+ );
+ });
+
+ it('should toggle the showNotifications checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Show system notifications'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('showNotifications', false);
+ });
+
+ it('should toggle the playSound checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Play sound'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('playSound', false);
+ });
+
+ it('should toggle the openAtStartup checkbox', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent.click(screen.getByLabelText('Open at startup'), {
+ target: { checked: true },
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false);
+ });
+});
diff --git a/src/components/settings/SystemSettings.tsx b/src/components/settings/SystemSettings.tsx
new file mode 100644
index 000000000..44e7a9363
--- /dev/null
+++ b/src/components/settings/SystemSettings.tsx
@@ -0,0 +1,66 @@
+import { type FC, useContext } from 'react';
+import { AppContext } from '../../context/App';
+import Constants from '../../utils/constants';
+import { isLinux, isMacOS } from '../../utils/platform';
+import { Checkbox } from '../fields/Checkbox';
+
+export const SystemSettings: FC = () => {
+ const { settings, updateSetting } = useContext(AppContext);
+
+ return (
+
+
+ System
+
+
+ updateSetting('keyboardShortcut', evt.target.checked)
+ }
+ tooltip={
+
+ When enabled you can use the hotkeys{' '}
+
+ {Constants.DEFAULT_KEYBOARD_SHORTCUT}
+ {' '}
+ to show or hide Gitify.
+
+ }
+ />
+ {isMacOS() && (
+
+ updateSetting('showNotificationsCountInTray', evt.target.checked)
+ }
+ />
+ )}
+
+ updateSetting('showNotifications', evt.target.checked)
+ }
+ />
+ updateSetting('playSound', evt.target.checked)}
+ />
+ {!isLinux() && (
+ updateSetting('openAtStartup', evt.target.checked)}
+ />
+ )}
+
+ );
+};
diff --git a/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap b/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap
new file mode 100644
index 000000000..ec5007e7e
--- /dev/null
+++ b/src/components/settings/__snapshots__/NotificationSettings.test.tsx.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`routes/components/NotificationSettings.tsx should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = `
+
+
+
+
+
+
+ Show notifications from Bot accounts
+
+
+
+
+
+
+
+
+`;
+
+exports[`routes/components/NotificationSettings.tsx should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = `
+
+
+
+
+
+
+ Show notifications from Bot accounts
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap
new file mode 100644
index 000000000..75e06fc2a
--- /dev/null
+++ b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`routes/components/SettingsFooter.tsx app version should show development app version 1`] = `
+
+ Gitify
+ dev
+
+`;
+
+exports[`routes/components/SettingsFooter.tsx app version should show production app version 1`] = `
+
+ Gitify
+ v0.0.1
+
+`;
diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx
index 333cc184e..962984fa6 100644
--- a/src/routes/Settings.test.tsx
+++ b/src/routes/Settings.test.tsx
@@ -3,7 +3,6 @@ import { MemoryRouter } from 'react-router-dom';
import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
import { mockPlatform } from '../__mocks__/utils';
import { AppContext } from '../context/App';
-import * as comms from '../utils/comms';
import { SettingsRoute } from './Settings';
const mockNavigate = jest.fn();
@@ -14,7 +13,6 @@ jest.mock('react-router-dom', () => ({
describe('routes/Settings.tsx', () => {
let originalPlatform: NodeJS.Platform;
- const updateSetting = jest.fn();
const fetchNotifications = jest.fn();
beforeAll(() => {
@@ -39,631 +37,39 @@ describe('routes/Settings.tsx', () => {
});
});
- describe('General', () => {
- it('should render itself & its children', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- expect(screen.getByTestId('settings')).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('Appearance section', () => {
- it('should change the theme radio group', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Light'));
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
- });
-
- it('should toggle detailed notifications checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- await screen.findByLabelText('Detailed notifications');
-
- fireEvent.click(screen.getByLabelText('Detailed notifications'));
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith(
- 'detailedNotifications',
- false,
+ it('should render itself & its children', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
);
});
- it('should toggle metric pills checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- await screen.findByLabelText('Show notification metric pills');
-
- fireEvent.click(screen.getByLabelText('Show notification metric pills'));
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('showPills', false);
- });
-
- it('should toggle account hostname checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- await screen.findByLabelText('Show account hostname');
-
- fireEvent.click(screen.getByLabelText('Show account hostname'));
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('showAccountHostname', true);
- });
+ expect(screen.getByTestId('settings')).toMatchSnapshot();
});
- describe('Notifications section', () => {
- it('should change the groupBy radio group', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Date'));
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('groupBy', 'DATE');
- });
- it('should toggle the showOnlyParticipating checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Show only participating'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('participating', false);
- });
-
- it('should open official docs for showOnlyParticipating tooltip', async () => {
- const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
-
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- const tooltipElement = screen.getByLabelText(
- 'tooltip-showOnlyParticipating',
- );
-
- fireEvent.mouseEnter(tooltipElement);
-
- fireEvent.click(
- screen.getByTitle(
- 'Open GitHub documentation for participating and watching notifications',
- ),
- );
-
- expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
- expect(openExternalLinkMock).toHaveBeenCalledWith(
- 'https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#about-participating-and-watching-notifications',
- );
- });
-
- 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'),
+ it('should go back by pressing the icon', async () => {
+ await act(async () => {
+ render(
+
+
+
+
+ ,
);
-
- // 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(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Mark as done on open'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('markAsDoneOnOpen', false);
- });
-
- it('should toggle the delayNotificationState checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Delay notification state'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith(
- 'delayNotificationState',
- false,
- );
- });
- });
-
- describe('System section', () => {
- it('should toggle the keyboardShortcut checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Enable keyboard shortcut'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('keyboardShortcut', false);
- });
-
- it('should toggle the showNotificationsCountInTray checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(
- screen.getByLabelText('Show notifications count in tray'),
- {
- target: { checked: true },
- },
- );
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith(
- 'showNotificationsCountInTray',
- false,
- );
- });
-
- it('should toggle the showNotifications checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Show system notifications'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('showNotifications', false);
- });
-
- it('should toggle the playSound checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Play sound'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('playSound', false);
- });
-
- it('should toggle the openAtStartup checkbox', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByLabelText('Open at startup'), {
- target: { checked: true },
- });
-
- expect(updateSetting).toHaveBeenCalledTimes(1);
- expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false);
- });
- });
-
- describe('Footer section', () => {
- describe('app version', () => {
- let originalEnv: NodeJS.ProcessEnv;
-
- beforeEach(() => {
- // Save the original node env state
- originalEnv = process.env;
- });
-
- afterEach(() => {
- // Restore the original node env state
- process.env = originalEnv;
- });
-
- it('should show production app version', async () => {
- process.env = {
- ...originalEnv,
- NODE_ENV: 'production',
- };
-
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- expect(screen.getByTitle('app-version')).toMatchSnapshot();
- });
-
- it('should show development app version', async () => {
- process.env = {
- ...originalEnv,
- NODE_ENV: 'development',
- };
-
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- expect(screen.getByTitle('app-version')).toMatchSnapshot();
- });
- });
-
- it('should open release notes', async () => {
- const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
-
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByTitle('View release notes'));
-
- expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
- expect(openExternalLinkMock).toHaveBeenCalledWith(
- 'https://github.com/gitify-app/gitify/releases/tag/v0.0.1',
- );
- });
-
- it('should open account management', async () => {
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByTitle('Accounts'));
- expect(mockNavigate).toHaveBeenCalledWith('/accounts');
- });
-
- it('should quit the app', async () => {
- const quitAppMock = jest.spyOn(comms, 'quitApp');
-
- await act(async () => {
- render(
-
-
-
-
- ,
- );
- });
-
- fireEvent.click(screen.getByTitle('Quit Gitify'));
- expect(quitAppMock).toHaveBeenCalledTimes(1);
- });
+ fireEvent.click(screen.getByLabelText('Go Back'));
+ expect(fetchNotifications).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
});
});
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index fada57163..878a6e1d7 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -1,328 +1,22 @@
-import {
- CheckIcon,
- CommentIcon,
- IssueClosedIcon,
- MilestoneIcon,
- PersonIcon,
- TagIcon,
- XCircleIcon,
-} from '@primer/octicons-react';
-import { ipcRenderer } from 'electron';
-import {
- type FC,
- type MouseEvent,
- useContext,
- useEffect,
- useState,
-} from 'react';
-import { useNavigate } from 'react-router-dom';
+import type { FC } from 'react';
import { Header } from '../components/Header';
-import { Checkbox } from '../components/fields/Checkbox';
-import { RadioGroup } from '../components/fields/RadioGroup';
-import { AppContext } from '../context/App';
-import { BUTTON_CLASS_NAME } from '../styles/gitify';
-import { GroupBy, Size, Theme } from '../types';
-import { getAppVersion, quitApp } from '../utils/comms';
-import Constants from '../utils/constants';
-import {
- openGitHubParticipatingDocs,
- openGitifyReleaseNotes,
-} from '../utils/links';
-import { isLinux, isMacOS } from '../utils/platform';
-import { setTheme } from '../utils/theme';
+import { AppearanceSettings } from '../components/settings/AppearanceSettings';
+import { NotificationSettings } from '../components/settings/NotificationSettings';
+import { SettingsFooter } from '../components/settings/SettingsFooter';
+import { SystemSettings } from '../components/settings/SystemSettings';
export const SettingsRoute: FC = () => {
- const { settings, updateSetting } = useContext(AppContext);
- const navigate = useNavigate();
-
- const [appVersion, setAppVersion] = useState(null);
-
- useEffect(() => {
- (async () => {
- if (process.env.NODE_ENV === 'development') {
- setAppVersion('dev');
- } else {
- const result = await getAppVersion();
- setAppVersion(`v${result}`);
- }
- })();
-
- ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => {
- if (settings.theme === Theme.SYSTEM) {
- setTheme(updatedTheme);
- }
- });
- }, [settings.theme]);
-
return (
-
-
-
- Appearance
-
- {
- updateSetting('theme', evt.target.value);
- }}
- />
-
- updateSetting('detailedNotifications', evt.target.checked)
- }
- tooltip={
-
-
- Enrich notifications with author or last commenter profile
- information, state and GitHub-like colors.
-
-
- ⚠️ Users with a large number of unread notifications may {' '}
- experience rate limiting under certain circumstances. Disable
- this setting if you experience this.
-
-
- }
- />
- updateSetting('showPills', evt.target.checked)}
- tooltip={
-
-
Show notification metric pills for:
-
-
-
-
- linked issues
-
-
- pr
- reviews
-
-
-
- comments
-
-
-
- labels
-
-
-
- milestones
-
-
-
-
- }
- />
-
- updateSetting('showAccountHostname', evt.target.checked)
- }
- />
-
-
-
-
- Notifications
-
- {
- updateSetting('groupBy', evt.target.value);
- }}
- />
-
- updateSetting('participating', evt.target.checked)
- }
- tooltip={
-
- See
- ) => {
- // Don't trigger onClick of parent element.
- event.stopPropagation();
- openGitHubParticipatingDocs();
- }}
- >
- official docs
-
- for more details.
-
- }
- />
-
- 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.
-
-
- }
- />
-
- updateSetting('markAsDoneOnOpen', evt.target.checked)
- }
- />
-
- updateSetting('delayNotificationState', evt.target.checked)
- }
- tooltip={
-
- Keep the notification within Gitify window upon interaction
- (click, mark as read, mark as done, etc) until the next refresh
- window (scheduled or user initiated).
-
- }
- />
-
-
-
-
- System
-
-
- updateSetting('keyboardShortcut', evt.target.checked)
- }
- tooltip={
-
- When enabled you can use the hotkeys{' '}
-
- {Constants.DEFAULT_KEYBOARD_SHORTCUT}
- {' '}
- to show or hide Gitify.
-
- }
- />
- {isMacOS() && (
-
- updateSetting(
- 'showNotificationsCountInTray',
- evt.target.checked,
- )
- }
- />
- )}
-
- updateSetting('showNotifications', evt.target.checked)
- }
- />
- updateSetting('playSound', evt.target.checked)}
- />
- {!isLinux() && (
-
- updateSetting('openAtStartup', evt.target.checked)
- }
- />
- )}
-
+
-
-
openGitifyReleaseNotes(appVersion)}
- >
- Gitify {appVersion}
-
-
-
{
- navigate('/accounts');
- }}
- >
-
-
-
-
-
-
-
-
+
);
};
diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap
index 7dc308869..d5d0b6cff 100644
--- a/src/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/routes/__snapshots__/Settings.test.tsx.snap
@@ -1,24 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`routes/Settings.tsx Footer section app version should show development app version 1`] = `
-
- Gitify
- dev
-
-`;
-
-exports[`routes/Settings.tsx Footer section app version should show production app version 1`] = `
-
- Gitify
- v0.0.1
-
-`;
-
-exports[`routes/Settings.tsx General should render itself & its children 1`] = `
+exports[`routes/Settings.tsx should render itself & its children 1`] = `
`;
-
-exports[`routes/Settings.tsx Notifications section should be able to toggle the showBots checkbox when detailedNotifications is enabled 1`] = `
-
-
-
-
-
-
- Show notifications from Bot accounts
-
-
-
-
-
-
-
-
-`;
-
-exports[`routes/Settings.tsx Notifications section should not be able to toggle the showBots checkbox when detailedNotifications is disabled 1`] = `
-
-
-
-
-
-
- Show notifications from Bot accounts
-
-
-
-
-
-
-
-
-`;