diff --git a/src/renderer/components/fields/RadioGroup.tsx b/src/renderer/components/fields/RadioGroup.tsx index e3b6a6f48..65122375f 100644 --- a/src/renderer/components/fields/RadioGroup.tsx +++ b/src/renderer/components/fields/RadioGroup.tsx @@ -17,9 +17,13 @@ export interface RadioGroupProps { } export function RadioGroup(props: Readonly) { + const firstOptionId = props.options[0] + ? `radio-${props.name}-${props.options[0].value.toLowerCase()}` + : props.name; + return ( - + {props.options.map((item) => { const name = `radio-${props.name}-${item.value.toLowerCase()}`; diff --git a/src/renderer/components/fields/__snapshots__/RadioGroup.test.tsx.snap b/src/renderer/components/fields/__snapshots__/RadioGroup.test.tsx.snap index 6b9374299..1520cada0 100644 --- a/src/renderer/components/fields/__snapshots__/RadioGroup.test.tsx.snap +++ b/src/renderer/components/fields/__snapshots__/RadioGroup.test.tsx.snap @@ -14,7 +14,7 @@ exports[`renderer/components/fields/RadioGroup.tsx > should render 1`] = ` > @@ -87,7 +87,7 @@ exports[`renderer/components/fields/RadioGroup.tsx > should render as disabled 1 > diff --git a/src/renderer/components/filters/AccountFilter.tsx b/src/renderer/components/filters/AccountFilter.tsx index 8a9ffbec0..73ce74936 100644 --- a/src/renderer/components/filters/AccountFilter.tsx +++ b/src/renderer/components/filters/AccountFilter.tsx @@ -5,7 +5,7 @@ import { useNotifications } from '../../hooks/useNotifications'; import { useAccountsStore, useFiltersStore } from '../../stores'; import { Checkbox } from '../fields/Checkbox'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; import { getAccountUUID } from '../../utils/auth/utils'; @@ -17,9 +17,13 @@ export function AccountFilter() { return (
- Filter notifications by account.</Text>}> + <SectionHeading + as="legend" + icon={PersonIcon} + tooltip={<Text>Filter notifications by account.</Text>} + > Account - + {accounts.map((account) => { diff --git a/src/renderer/components/filters/FilterSection.tsx b/src/renderer/components/filters/FilterSection.tsx index e62d7426a..6480217e7 100644 --- a/src/renderer/components/filters/FilterSection.tsx +++ b/src/renderer/components/filters/FilterSection.tsx @@ -7,7 +7,7 @@ import { useNotifications } from '../../hooks/useNotifications'; import { type FiltersState, useFiltersStore, useSettingsStore } from '../../stores'; import { Checkbox } from '../fields/Checkbox'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; import type { Filter } from '../../utils/notifications/filters'; import { RequiresDetailedNotificationWarning } from './RequiresDetailedNotificationsWarning'; @@ -50,7 +50,8 @@ function FilterSectionComponent({ return (
- ({ } > {title} - + {(Object.keys(filter.FILTER_TYPES) as FiltersState[K][number][]) diff --git a/src/renderer/components/filters/SearchFilter.tsx b/src/renderer/components/filters/SearchFilter.tsx index 961fc083d..47647cadc 100644 --- a/src/renderer/components/filters/SearchFilter.tsx +++ b/src/renderer/components/filters/SearchFilter.tsx @@ -14,7 +14,7 @@ import { cn } from 'cn'; import { useFiltersStore, useSettingsStore } from '../../stores'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; import { IconColor, type SearchToken, Size } from '../../types'; @@ -66,7 +66,8 @@ export function SearchFilter() { return (
- @@ -109,7 +110,7 @@ export function SearchFilter() { } > Search - + should render itself &
- + + FilterSectionTitle + +
- + + +

+ FilterSectionTitle +

- +
should render itself &
- + + FilterSectionTitle + +
- + + +

+ FilterSectionTitle +

- +
should render itself & i
- + + Reason + +
- +
- +
should render
- + + Review Request Type + +
- +
- +
should render itself & it
- + + State + +
- +
- +
should render itsel
- + + Type + +
- +
- +
should render itself &
- + + User Type + +
- +
- +
) { variant="invisible" /> - - {props.children} - + {props.children}
); diff --git a/src/renderer/components/primitives/SectionHeading.test.tsx b/src/renderer/components/primitives/SectionHeading.test.tsx new file mode 100644 index 000000000..5f31ad516 --- /dev/null +++ b/src/renderer/components/primitives/SectionHeading.test.tsx @@ -0,0 +1,37 @@ +import { PersonFillIcon } from '@primer/octicons-react'; + +import { renderWithProviders } from '../../__helpers__/test-utils'; + +import { SectionHeading } from './SectionHeading'; + +describe('renderer/components/primitives/SectionHeading.tsx', () => { + it('should render a heading by default', async () => { + const { container } = renderWithProviders( + Heading, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render a legend for a fieldset', async () => { + const { container } = renderWithProviders( +
+ + Legend + +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render a specific heading level', async () => { + const { container } = renderWithProviders( + + Heading + , + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/renderer/components/primitives/SectionHeading.tsx b/src/renderer/components/primitives/SectionHeading.tsx new file mode 100644 index 000000000..fd284fb0d --- /dev/null +++ b/src/renderer/components/primitives/SectionHeading.tsx @@ -0,0 +1,56 @@ +import type { ReactNode } from 'react'; + +import type { Icon } from '@primer/octicons-react'; +import { Heading, Stack } from '@primer/react'; + +import { Tooltip } from '../fields/Tooltip'; + +type SectionHeadingTag = 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + +interface SectionHeadingProps { + icon: Icon; + children: string; + /** + * Render a heading for standalone use, or a legend when the heading labels a + * fieldset. Defaults to a level-4 heading. + */ + as?: SectionHeadingTag | 'legend'; + tooltip?: ReactNode; +} + +export function SectionHeading({ as = 'h4', ...props }: Readonly) { + const name = props.children.toLowerCase().replaceAll(' ', '-'); + const headingTag = as === 'legend' ? 'h4' : as; + + const tooltip = props.tooltip && ( + + {props.tooltip} + + } + /> + ); + + const content = ( +
+ + + {props.children} + {tooltip} + +
+ ); + + if (as === 'legend') { + return ( + <> + {props.children} + {content} + + ); + } + + return content; +} diff --git a/src/renderer/components/primitives/Title.test.tsx b/src/renderer/components/primitives/Title.test.tsx deleted file mode 100644 index 09a73ddb7..000000000 --- a/src/renderer/components/primitives/Title.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { PersonFillIcon } from '@primer/octicons-react'; - -import { renderWithProviders } from '../../__helpers__/test-utils'; - -import { Title } from './Title'; - -describe('renderer/routes/components/primitives/Title.tsx', () => { - it('should render the title - default size', async () => { - const { container } = renderWithProviders(Legend); - - expect(container).toMatchSnapshot(); - }); - - it('should render the title - specific size', async () => { - const { container } = renderWithProviders( - - Legend - , - ); - - expect(container).toMatchSnapshot(); - }); -}); diff --git a/src/renderer/components/primitives/Title.tsx b/src/renderer/components/primitives/Title.tsx deleted file mode 100644 index 20f8d661c..000000000 --- a/src/renderer/components/primitives/Title.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { ReactNode } from 'react'; - -import type { Icon } from '@primer/octicons-react'; -import { Heading, Stack } from '@primer/react'; - -import { Tooltip } from '../fields/Tooltip'; - -interface TitleProps { - icon: Icon; - children: string; - size?: number; - tooltip?: ReactNode; -} - -export function Title({ size: _size = 2, ...props }: Readonly) { - const name = props.children.toLowerCase().replaceAll(' ', '-'); - - return ( - -
- - - {props.children} - {props.tooltip && ( - - {props.tooltip} - - } - /> - )} - -
-
- ); -} diff --git a/src/renderer/components/primitives/__snapshots__/Header.test.tsx.snap b/src/renderer/components/primitives/__snapshots__/Header.test.tsx.snap index 235e9e841..3f6ab80c7 100644 --- a/src/renderer/components/primitives/__snapshots__/Header.test.tsx.snap +++ b/src/renderer/components/primitives/__snapshots__/Header.test.tsx.snap @@ -43,47 +43,45 @@ exports[`renderer/components/primitives/Header.tsx > should render itself & its /> - +
- + Test Header +
- +
diff --git a/src/renderer/components/primitives/__snapshots__/SectionHeading.test.tsx.snap b/src/renderer/components/primitives/__snapshots__/SectionHeading.test.tsx.snap new file mode 100644 index 000000000..42da7a56a --- /dev/null +++ b/src/renderer/components/primitives/__snapshots__/SectionHeading.test.tsx.snap @@ -0,0 +1,140 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renderer/components/primitives/SectionHeading.tsx > should render a heading by default 1`] = ` +
+
+
+ +

+ Heading +

+
+
+
+`; + +exports[`renderer/components/primitives/SectionHeading.tsx > should render a legend for a fieldset 1`] = ` +
+
+ + Legend + +
+
+ +

+ Legend +

+
+
+
+
+`; + +exports[`renderer/components/primitives/SectionHeading.tsx > should render a specific heading level 1`] = ` +
+
+
+ +

+ Heading +

+
+
+
+`; diff --git a/src/renderer/components/primitives/__snapshots__/Title.test.tsx.snap b/src/renderer/components/primitives/__snapshots__/Title.test.tsx.snap deleted file mode 100644 index 38f66999e..000000000 --- a/src/renderer/components/primitives/__snapshots__/Title.test.tsx.snap +++ /dev/null @@ -1,93 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`renderer/routes/components/primitives/Title.tsx > should render the title - default size 1`] = ` -
- -
-
- -

- Legend -

-
-
-
-
-`; - -exports[`renderer/routes/components/primitives/Title.tsx > should render the title - specific size 1`] = ` -
- -
-
- -

- Legend -

-
-
-
-
-`; diff --git a/src/renderer/components/settings/AlertSettings.test.tsx b/src/renderer/components/settings/AlertSettings.test.tsx new file mode 100644 index 000000000..3d13f20d2 --- /dev/null +++ b/src/renderer/components/settings/AlertSettings.test.tsx @@ -0,0 +1,83 @@ +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { renderWithProviders } from '../../__helpers__/test-utils'; +import { mockSettings } from '../../__mocks__/state-mocks'; + +import { useSettingsStore } from '../../stores'; + +import type { Percentage } from '../../types'; + +import { AlertSettings } from './AlertSettings'; + +describe('renderer/components/settings/AlertSettings.tsx', () => { + let toggleSettingSpy: ReturnType; + let updateSettingSpy: ReturnType; + + beforeEach(() => { + toggleSettingSpy = vi.spyOn(useSettingsStore.getState(), 'toggleSetting'); + updateSettingSpy = vi.spyOn(useSettingsStore.getState(), 'updateSetting'); + }); + + it.each([ + ['checkbox-showNotifications', 'showNotifications'], + ['checkbox-playSound', 'playSound'], + ['checkbox-showUpdateNotifications', 'showUpdateNotifications'], + ] as const)('should toggle %s checkbox', async (testId, setting) => { + await act(async () => { + renderWithProviders(); + }); + + await userEvent.click(screen.getByTestId(testId)); + + expect(toggleSettingSpy).toHaveBeenCalledTimes(1); + expect(toggleSettingSpy).toHaveBeenCalledWith(setting); + }); + + describe('playSound', () => { + it('volume controls should not be shown if playSound checkbox is false', async () => { + renderWithProviders(, { + settings: { ...mockSettings, playSound: false }, + }); + + expect(screen.getByTestId('settings-volume-group')).not.toBeVisible(); + }); + + it('volume controls should be shown if playSound checkbox is true', async () => { + renderWithProviders(, { + settings: { ...mockSettings, playSound: true }, + }); + + expect(screen.getByTestId('settings-volume-group')).toBeVisible(); + }); + + it('should increase notification volume', async () => { + renderWithProviders(); + + await userEvent.click(screen.getByTestId('settings-volume-up')); + + expect(updateSettingSpy).toHaveBeenCalledTimes(1); + expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 30); + }); + + it('should decrease notification volume', async () => { + renderWithProviders(); + + await userEvent.click(screen.getByTestId('settings-volume-down')); + + expect(updateSettingSpy).toHaveBeenCalledTimes(1); + expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 10); + }); + + it('should reset notification volume', async () => { + renderWithProviders(, { + settings: { ...mockSettings, notificationVolume: 30 as Percentage }, + }); + + await userEvent.click(screen.getByTestId('settings-volume-reset')); + + expect(updateSettingSpy).toHaveBeenCalledTimes(1); + expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 20); + }); + }); +}); diff --git a/src/renderer/components/settings/AlertSettings.tsx b/src/renderer/components/settings/AlertSettings.tsx new file mode 100644 index 000000000..4fe0c575a --- /dev/null +++ b/src/renderer/components/settings/AlertSettings.tsx @@ -0,0 +1,119 @@ +import { BellIcon, SyncIcon } from '@primer/octicons-react'; +import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react'; + +import { APPLICATION } from '../../../shared/constants'; + +import { DEFAULT_SETTINGS_STATE, useSettingsStore } from '../../stores'; + +import { Checkbox } from '../fields/Checkbox'; +import { SectionHeading } from '../primitives/SectionHeading'; + +import { VolumeDownIcon } from '../../icons/VolumeDownIcon'; +import { VolumeUpIcon } from '../../icons/VolumeUpIcon'; +import { + canDecreaseVolume, + canIncreaseVolume, + decreaseVolume, + increaseVolume, +} from '../../utils/ui/volume'; + +const defaultSettings = DEFAULT_SETTINGS_STATE; + +export function AlertSettings() { + const toggleSetting = useSettingsStore((s) => s.toggleSetting); + const updateSetting = useSettingsStore((s) => s.updateSetting); + + const showNotifications = useSettingsStore((s) => s.showNotifications); + const showUpdateNotifications = useSettingsStore((s) => s.showUpdateNotifications); + const playSound = useSettingsStore((s) => s.playSound); + const notificationVolume = useSettingsStore((s) => s.notificationVolume); + + return ( +
+ + Alerts + + + + toggleSetting('showNotifications')} + tooltip={ + Display native operating system notifications for new unread notifications. + } + /> + + + toggleSetting('playSound')} + /> + + + + + toggleSetting('showUpdateNotifications')} + tooltip={ + + Show a notification when a {APPLICATION.NAME} update is ready. Updates will still be + checked and shown in the menu bar. + + } + /> + +
+ ); +} diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx index d776e334b..d5d85a14f 100644 --- a/src/renderer/components/settings/AppearanceSettings.test.tsx +++ b/src/renderer/components/settings/AppearanceSettings.test.tsx @@ -2,12 +2,8 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../__helpers__/test-utils'; -import { - mockGitHubAppAccount, - mockGitHubEnterpriseServerAccount, -} from '../../__mocks__/account-mocks'; -import { useAccountsStore, useSettingsStore } from '../../stores'; +import { useSettingsStore } from '../../stores'; import { DesignLanguage } from '../../types'; @@ -84,21 +80,15 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { expect(zoomResetSpy).toHaveBeenCalledTimes(1); }); - it.each([ - ['checkbox-increaseContrast', 'increaseContrast'], - ['checkbox-showAccountHeader', 'showAccountHeader'], - ['checkbox-wrapNotificationTitle', 'wrapNotificationTitle'], - ] as const)('should toggle %s checkbox', async (testId, setting) => { + it('should toggle the increase contrast checkbox', async () => { await act(async () => { - renderWithProviders(, { - accounts: [mockGitHubAppAccount], - }); + renderWithProviders(); }); - await userEvent.click(screen.getByTestId(testId)); + await userEvent.click(screen.getByTestId('checkbox-increaseContrast')); expect(toggleSettingSpy).toHaveBeenCalledTimes(1); - expect(toggleSettingSpy).toHaveBeenCalledWith(setting); + expect(toggleSettingSpy).toHaveBeenCalledWith('increaseContrast'); }); it('hides the increase contrast checkbox for Glass', async () => { @@ -131,22 +121,4 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { expect(screen.queryByTestId('checkbox-showStatusIconColors')).not.toBeInTheDocument(); }); - - it('reactively hides the account header checkbox when a second account is added', async () => { - await act(async () => { - renderWithProviders(, { - accounts: [mockGitHubAppAccount], - }); - }); - - expect(screen.getByTestId('checkbox-showAccountHeader')).toBeInTheDocument(); - - await act(async () => { - useAccountsStore.setState({ - accounts: [mockGitHubAppAccount, mockGitHubEnterpriseServerAccount], - }); - }); - - expect(screen.queryByTestId('checkbox-showAccountHeader')).not.toBeInTheDocument(); - }); }); diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx index f025c2261..ded78d4c2 100644 --- a/src/renderer/components/settings/AppearanceSettings.tsx +++ b/src/renderer/components/settings/AppearanceSettings.tsx @@ -1,11 +1,11 @@ import { PaintbrushIcon, SyncIcon, ZoomInIcon, ZoomOutIcon } from '@primer/octicons-react'; import { Button, ButtonGroup, IconButton, Select, Stack, Text } from '@primer/react'; -import { useAccountsStore, useSettingsStore } from '../../stores'; +import { useSettingsStore } from '../../stores'; import { Checkbox } from '../fields/Checkbox'; import { FieldLabel } from '../fields/FieldLabel'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; import { DesignLanguage, Theme } from '../../types'; @@ -19,9 +19,6 @@ import { } from '../../utils/ui/zoom'; export function AppearanceSettings() { - // Account store values - const hasMultipleAccounts = useAccountsStore((s) => s.hasMultipleAccounts()); - // Setting store actions const toggleSetting = useSettingsStore((s) => s.toggleSetting); const updateSetting = useSettingsStore((s) => s.updateSetting); @@ -33,13 +30,13 @@ export function AppearanceSettings() { const showStatusIconColors = useSettingsStore((s) => s.showStatusIconColors); const colorModeSupported = (mode: Theme) => supportedColorModes(designLanguage).includes(mode); - const showAccountHeader = useSettingsStore((s) => s.showAccountHeader); - const wrapNotificationTitle = useSettingsStore((s) => s.wrapNotificationTitle); const zoomPercentage = useSettingsStore((s) => s.zoomPercentage); return (
- Appearance + + Appearance + @@ -160,34 +157,6 @@ export function AppearanceSettings() { /> - - toggleSetting('showAccountHeader')} - tooltip={ - - When enabled, displays an account header (avatar, username and quick links) above the - notifications list. - - } - visible={!hasMultipleAccounts} - /> - - toggleSetting('wrapNotificationTitle')} - tooltip={ - - Wrap long notification titles onto multiple lines instead of truncating with an - ellipsis. This shows the full title but may increase the height of the notification - list. - - } - />
); diff --git a/src/renderer/components/settings/NotificationSettings.test.tsx b/src/renderer/components/settings/InboxSettings.test.tsx similarity index 69% rename from src/renderer/components/settings/NotificationSettings.test.tsx rename to src/renderer/components/settings/InboxSettings.test.tsx index 50cd2526c..6f00f37ad 100644 --- a/src/renderer/components/settings/NotificationSettings.test.tsx +++ b/src/renderer/components/settings/InboxSettings.test.tsx @@ -2,16 +2,21 @@ import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../__helpers__/test-utils'; +import { + mockGitHubCloudAccount, + mockGitHubEnterpriseServerAccount, + mockGitLabAccount, +} from '../../__mocks__/account-mocks'; import { mockSettings } from '../../__mocks__/state-mocks'; import { Constants } from '../../constants'; -import { useSettingsStore } from '../../stores'; +import { useAccountsStore, useSettingsStore } from '../../stores'; import * as comms from '../../utils/system/comms'; -import { NotificationSettings } from './NotificationSettings'; +import { InboxSettings } from './InboxSettings'; -describe('renderer/components/settings/NotificationSettings.tsx', () => { +describe('renderer/components/settings/InboxSettings.tsx', () => { let toggleSettingSpy: ReturnType; let updateSettingSpy: ReturnType; @@ -22,7 +27,7 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { it('should change the groupBy radio group', async () => { await act(async () => { - renderWithProviders(); + renderWithProviders(); }); await userEvent.click(screen.getByTestId('radio-groupBy-date')); @@ -34,7 +39,7 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { describe('fetch interval settings', () => { it('should update the fetch interval values when using the buttons', async () => { await act(async () => { - renderWithProviders(); + renderWithProviders(); }); // Increase fetch interval @@ -71,7 +76,7 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { it('should prevent going lower than minimum interval', async () => { await act(async () => { - renderWithProviders(, { + renderWithProviders(, { settings: { ...mockSettings, fetchInterval: @@ -98,7 +103,7 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { it('should prevent going above maximum interval', async () => { await act(async () => { - renderWithProviders(, { + renderWithProviders(, { settings: { ...mockSettings, fetchInterval: @@ -133,10 +138,14 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { ['checkbox-fetchReadNotifications', 'fetchReadNotifications'], ['checkbox-markAsDoneOnOpen', 'markAsDoneOnOpen'], ['checkbox-markAsDoneOnUnsubscribe', 'markAsDoneOnUnsubscribe'], + ['checkbox-showAccountHeader', 'showAccountHeader'], + ['checkbox-wrapNotificationTitle', 'wrapNotificationTitle'], ['checkbox-delayNotificationState', 'delayNotificationState'], ] as const)('should toggle %s checkbox', async (testId, setting) => { await act(async () => { - renderWithProviders(); + renderWithProviders(, { + accounts: [mockGitHubCloudAccount], + }); }); await userEvent.click(screen.getByTestId(testId)); @@ -145,11 +154,62 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { expect(toggleSettingSpy).toHaveBeenCalledWith(setting); }); + it('reactively hides the account header checkbox when a second account is added', async () => { + await act(async () => { + renderWithProviders(, { + accounts: [mockGitHubCloudAccount], + }); + }); + + expect(screen.getByTestId('checkbox-showAccountHeader')).toBeInTheDocument(); + + await act(async () => { + useAccountsStore.setState({ + accounts: [mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount], + }); + }); + + expect(screen.queryByTestId('checkbox-showAccountHeader')).not.toBeInTheDocument(); + }); + + describe('GitHub-specific settings', () => { + it('hides GitHub-only settings when no GitHub account is connected', async () => { + await act(async () => { + renderWithProviders(, { + accounts: [mockGitLabAccount], + }); + }); + + expect(screen.queryByTestId('checkbox-showOnlyParticipating')).not.toBeInTheDocument(); + expect( + screen.getByText('GitHub-specific options require a GitHub account.'), + ).toBeInTheDocument(); + }); + + it('disables mark as done when the GitHub account does not support it', async () => { + await act(async () => { + renderWithProviders(, { + accounts: [mockGitHubEnterpriseServerAccount], + }); + }); + + expect(screen.getByTestId('checkbox-markAsDoneOnOpen')).toBeDisabled(); + expect(screen.getByTestId('checkbox-markAsDoneOnUnsubscribe')).toBeDisabled(); + expect( + screen.getByText( + 'Mark as done requires GitHub Cloud or GitHub Enterprise Server 3.13 or later.', + ), + ).toBeInTheDocument(); + }); + }); + it('should open official docs for showOnlyParticipating tooltip', async () => { const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn()); await act(async () => { - renderWithProviders(); + renderWithProviders(, { + accounts: [mockGitHubCloudAccount], + }); }); const tooltipElement = screen.getByLabelText('tooltip-showOnlyParticipating'); diff --git a/src/renderer/components/settings/InboxSettings.tsx b/src/renderer/components/settings/InboxSettings.tsx new file mode 100644 index 000000000..06f2986d3 --- /dev/null +++ b/src/renderer/components/settings/InboxSettings.tsx @@ -0,0 +1,484 @@ +import type { MouseEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { + AlertFillIcon, + CheckIcon, + CommentIcon, + DashIcon, + DownloadIcon, + GitCommitIcon, + GitPullRequestIcon, + InboxIcon, + IssueOpenedIcon, + ListUnorderedIcon, + MarkGithubIcon, + MilestoneIcon, + PlusIcon, + ReleaseIcon, + SmileyIcon, + SyncIcon, + TagIcon, +} from '@primer/octicons-react'; +import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react'; + +import { formatDuration } from 'date-fns/formatDuration'; +import { millisecondsToMinutes } from 'date-fns/millisecondsToMinutes'; + +import { APPLICATION } from '../../../shared/constants'; + +import { Constants } from '../../constants'; + +import { useAccountsStore, useSettingsStore } from '../../stores'; + +import { Checkbox } from '../fields/Checkbox'; +import { FieldLabel } from '../fields/FieldLabel'; +import { RadioGroup } from '../fields/RadioGroup'; +import { SectionHeading } from '../primitives/SectionHeading'; + +import { GroupBy, Size } from '../../types'; + +import { isMarkAsDoneFeatureSupported } from '../../utils/api/features'; +import { hasAlternateScopes, hasRecommendedScopes } from '../../utils/auth/scopes'; +import { openGitHubParticipatingDocs } from '../../utils/system/links'; +import { SettingsGroupHeading } from './SettingsGroupHeading'; + +export function InboxSettings() { + const navigate = useNavigate(); + + // Account store values + const accounts = useAccountsStore((s) => s.accounts); + const hasMultipleAccounts = useAccountsStore((s) => s.hasMultipleAccounts()); + const hasGitHubAccount = accounts.some((account) => account.forge === 'github'); + const hasMarkAsDoneAccount = accounts.some(isMarkAsDoneFeatureSupported); + + // Setting store actions + const toggleSetting = useSettingsStore((s) => s.toggleSetting); + const updateSetting = useSettingsStore((s) => s.updateSetting); + + // Setting store values + const fetchInterval = useSettingsStore((s) => s.fetchInterval); + const fetchAllNotifications = useSettingsStore((s) => s.fetchAllNotifications); + const fetchReadNotifications = useSettingsStore((s) => s.fetchReadNotifications); + const participating = useSettingsStore((s) => s.participating); + const detailedNotifications = useSettingsStore((s) => s.detailedNotifications); + const showPills = useSettingsStore((s) => s.showPills); + const showNumber = useSettingsStore((s) => s.showNumber); + const groupBy = useSettingsStore((s) => s.groupBy); + const showAccountHeader = useSettingsStore((s) => s.showAccountHeader); + const wrapNotificationTitle = useSettingsStore((s) => s.wrapNotificationTitle); + const markAsDoneOnOpen = useSettingsStore((s) => s.markAsDoneOnOpen); + const markAsDoneOnUnsubscribe = useSettingsStore((s) => s.markAsDoneOnUnsubscribe); + const delayNotificationState = useSettingsStore((s) => s.delayNotificationState); + + return ( +
+ + Inbox + + + + Fetching + + + + + + { + const newInterval = Math.max( + fetchInterval - Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, + Constants.MIN_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + + if (newInterval !== fetchInterval) { + updateSetting('fetchInterval', newInterval); + } + }} + size="small" + unsafeDisableTooltip={true} + /> + + + + { + const newInterval = Math.min( + fetchInterval + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, + Constants.MAX_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + + if (newInterval !== fetchInterval) { + updateSetting('fetchInterval', newInterval); + } + }} + size="small" + unsafeDisableTooltip={true} + /> + + { + updateSetting('fetchInterval', Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS); + }} + size="small" + unsafeDisableTooltip={true} + variant="danger" + /> + + + + toggleSetting('fetchAllNotifications')} + tooltip={ + + + When checked, {APPLICATION.NAME} will fetch{' '} + all notifications from your inbox. + + + When unchecked, {APPLICATION.NAME} will fetch the first page of + notifications (max 50 records per GitHub account) + + + } + /> + + toggleSetting('fetchReadNotifications')} + tooltip={ + + Fetch all notifications including read and done. + + ⚠️ GitHub's API does not distinguish between read and done states, so 'Mark as done' + actions will be unavailable when this setting is enabled. + + + ⚠️ Enabling this setting will increase API usage and may cause rate limiting for + users with many notifications. + + + } + /> + + List + + { + updateSetting('groupBy', evt.target.value as GroupBy); + }} + options={[ + { label: 'Repository', value: GroupBy.REPOSITORY }, + { label: 'Date', value: GroupBy.DATE }, + ]} + tooltip={ + + Choose how notifications are displayed in the list. + + Repository groups notifications by their repository full + name. + + + Date shows notifications in chronological order. + + + } + value={groupBy} + /> + + toggleSetting('showAccountHeader')} + tooltip={ + + When enabled, displays an account header (avatar, username and quick links) above the + notifications list. + + } + visible={!hasMultipleAccounts} + /> + + toggleSetting('wrapNotificationTitle')} + tooltip={ + + Wrap long notification titles onto multiple lines instead of truncating with an + ellipsis. This shows the full title but may increase the height of the notification + list. + + } + /> + + toggleSetting('showNumber')} + tooltip={ + + Show the notification reference where available: +
+ + + + Discussion + #123 + + + + + Issue + #123 + + + + + Pull Request + #123 + + + + + Commit + a1b2c3d + + + + + Release + v1.2.3 + + +
+
+ } + /> + + toggleSetting('delayNotificationState')} + tooltip={ + + Keep the notification within {APPLICATION.NAME} upon interaction (ie: open + notification, mark as read, mark as done) until the next refresh window (scheduled or + user initiated). + + } + /> + + {hasGitHubAccount ? ( + <> + GitHub + + toggleSetting('participating')} + tooltip={ + + + When checked, {APPLICATION.NAME} will fetch only + participating notifications. + + + When unchecked, {APPLICATION.NAME} will fetch participating + and watching notifications. + + + See{' '} + {' '} + for more details. + + + } + /> + + toggleSetting('detailedNotifications')} + tooltip={ + + + When checked, {APPLICATION.NAME} will enrich notifications + with detailed user and state information. You may also choose to display{' '} + notification metrics. + + + When unchecked, {APPLICATION.NAME} will only fetch basic + notification details. + + + ⚠️ Users with a large number of unread notifications may experience rate + limiting under certain circumstances. Please disable this setting if you + experience this. + + + } + /> + + {detailedNotifications && + accounts.some( + (account) => + account.forge === 'github' && + !hasRecommendedScopes(account) && + !hasAlternateScopes(account), + ) && ( + + + + + One or more accounts are missing scopes required for notification enrichment. + Notification details may be incomplete. + + + Visit{' '} + {' '} + to review. + + + + )} + + + + toggleSetting('markAsDoneOnOpen')} + tooltip={ + + Mark as done is supported in GitHub Cloud and GitHub + Enterprise Server 3.13 or later. + + } + /> + + toggleSetting('markAsDoneOnUnsubscribe')} + tooltip={ + + Mark as done is supported in GitHub Cloud and GitHub + Enterprise Server 3.13 or later. + + } + /> + + {!hasMarkAsDoneAccount && ( + + Mark as done requires GitHub Cloud or GitHub Enterprise Server 3.13 or later. + + )} + + ) : ( + + GitHub-specific options require a GitHub account. + + )} +
+
+ ); +} diff --git a/src/renderer/components/settings/NotificationSettings.tsx b/src/renderer/components/settings/NotificationSettings.tsx deleted file mode 100644 index 4447cd5bb..000000000 --- a/src/renderer/components/settings/NotificationSettings.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import type { MouseEvent } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { - AlertFillIcon, - BellIcon, - CheckIcon, - CommentIcon, - DashIcon, - GitCommitIcon, - GitPullRequestIcon, - IssueOpenedIcon, - MilestoneIcon, - PlusIcon, - ReleaseIcon, - SmileyIcon, - SyncIcon, - TagIcon, -} from '@primer/octicons-react'; -import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react'; - -import { formatDuration } from 'date-fns/formatDuration'; -import { millisecondsToMinutes } from 'date-fns/millisecondsToMinutes'; - -import { APPLICATION } from '../../../shared/constants'; - -import { Constants } from '../../constants'; - -import { useAccountsStore, useSettingsStore } from '../../stores'; - -import { Checkbox } from '../fields/Checkbox'; -import { FieldLabel } from '../fields/FieldLabel'; -import { RadioGroup } from '../fields/RadioGroup'; -import { Title } from '../primitives/Title'; - -import { GroupBy, Size } from '../../types'; - -import { hasAlternateScopes, hasRecommendedScopes } from '../../utils/auth/scopes'; -import { openGitHubParticipatingDocs } from '../../utils/system/links'; - -export function NotificationSettings() { - const navigate = useNavigate(); - - // Account store values - const accounts = useAccountsStore((s) => s.accounts); - - // Setting store actions - const toggleSetting = useSettingsStore((s) => s.toggleSetting); - const updateSetting = useSettingsStore((s) => s.updateSetting); - - // Setting store values - const groupBy = useSettingsStore((s) => s.groupBy); - const fetchInterval = useSettingsStore((s) => s.fetchInterval); - const fetchAllNotifications = useSettingsStore((s) => s.fetchAllNotifications); - const detailedNotifications = useSettingsStore((s) => s.detailedNotifications); - const showPills = useSettingsStore((s) => s.showPills); - const showNumber = useSettingsStore((s) => s.showNumber); - const participating = useSettingsStore((s) => s.participating); - const fetchReadNotifications = useSettingsStore((s) => s.fetchReadNotifications); - const markAsDoneOnOpen = useSettingsStore((s) => s.markAsDoneOnOpen); - const markAsDoneOnUnsubscribe = useSettingsStore((s) => s.markAsDoneOnUnsubscribe); - const delayNotificationState = useSettingsStore((s) => s.delayNotificationState); - - return ( -
- Notifications - - - { - updateSetting('groupBy', evt.target.value as GroupBy); - }} - options={[ - { label: 'Repository', value: GroupBy.REPOSITORY }, - { label: 'Date', value: GroupBy.DATE }, - ]} - tooltip={ - - Choose how notifications are displayed in the list. - - Repository groups notifications by their repository full - name. - - - Date shows notifications in chronological order. - - - } - value={groupBy} - /> - - - - - - { - const newInterval = Math.max( - fetchInterval - Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, - Constants.MIN_FETCH_NOTIFICATIONS_INTERVAL_MS, - ); - - if (newInterval !== fetchInterval) { - updateSetting('fetchInterval', newInterval); - } - }} - size="small" - unsafeDisableTooltip={true} - /> - - - - { - const newInterval = Math.min( - fetchInterval + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, - Constants.MAX_FETCH_NOTIFICATIONS_INTERVAL_MS, - ); - - if (newInterval !== fetchInterval) { - updateSetting('fetchInterval', newInterval); - } - }} - size="small" - unsafeDisableTooltip={true} - /> - - { - updateSetting('fetchInterval', Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS); - }} - size="small" - unsafeDisableTooltip={true} - variant="danger" - /> - - - - toggleSetting('fetchAllNotifications')} - tooltip={ - - - When checked, {APPLICATION.NAME} will fetch{' '} - all notifications from your inbox. - - - When unchecked, {APPLICATION.NAME} will fetch the first page of - notifications (max 50 records per GitHub account) - - - } - /> - - toggleSetting('detailedNotifications')} - tooltip={ - - - When checked, {APPLICATION.NAME} will enrich notifications with - detailed user and state information. You may also choose to display{' '} - notification metric pills or{' '} - notification reference numbers. - - - When unchecked, {APPLICATION.NAME} will only fetch basic - notification details. - - - ⚠️ Users with a large number of unread notifications may experience rate - limiting under certain circumstances. Please disable this setting if you experience - this. - - - } - /> - - {detailedNotifications && - accounts.some( - (account) => !hasRecommendedScopes(account) && !hasAlternateScopes(account), - ) && ( - - - - - One or more accounts are missing scopes required for notification enrichment. - Notification details may be incomplete. - - - Visit{' '} - {' '} - to review. - - - - )} - - - - toggleSetting('participating')} - tooltip={ - - - When checked, {APPLICATION.NAME} will fetch only participating - notifications. - - - When unchecked, {APPLICATION.NAME} will fetch participating and - watching notifications. - - - See{' '} - {' '} - for more details. - - - } - /> - - toggleSetting('fetchReadNotifications')} - tooltip={ - - Fetch all notifications including read and done. - - ⚠️ GitHub's API does not distinguish between read and done states, so 'Mark as done' - actions will be unavailable when this setting is enabled. - - - ⚠️ Enabling this setting will increase API usage and may cause rate limiting for - users with many notifications. - - - } - /> - - toggleSetting('markAsDoneOnOpen')} - tooltip={ - - Mark as done feature is supported in GitHub Cloud and GitHub - Enterprise Server 3.13 or later. - - } - /> - - toggleSetting('markAsDoneOnUnsubscribe')} - tooltip={ - - Mark as done feature is supported in GitHub Cloud and GitHub - Enterprise Server 3.13 or later. - - } - /> - - toggleSetting('delayNotificationState')} - tooltip={ - - Keep the notification within {APPLICATION.NAME} upon interaction (ie: open - notification, mark as read, mark as done) until the next refresh window (scheduled or - user initiated). - - } - /> - -
- ); -} diff --git a/src/renderer/components/settings/SettingsGroupHeading.test.tsx b/src/renderer/components/settings/SettingsGroupHeading.test.tsx new file mode 100644 index 000000000..0a98eb95c --- /dev/null +++ b/src/renderer/components/settings/SettingsGroupHeading.test.tsx @@ -0,0 +1,15 @@ +import { DownloadIcon } from '@primer/octicons-react'; + +import { renderWithProviders } from '../../__helpers__/test-utils'; + +import { SettingsGroupHeading } from './SettingsGroupHeading'; + +describe('renderer/components/settings/SettingsGroupHeading.tsx', () => { + it('should render a group heading', async () => { + const { container } = renderWithProviders( + Fetching, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/renderer/components/settings/SettingsGroupHeading.tsx b/src/renderer/components/settings/SettingsGroupHeading.tsx new file mode 100644 index 000000000..89f32340d --- /dev/null +++ b/src/renderer/components/settings/SettingsGroupHeading.tsx @@ -0,0 +1,21 @@ +import type { Icon } from '@primer/octicons-react'; +import { Stack, Text } from '@primer/react'; + +interface SettingsGroupHeadingProps { + icon: Icon; + children: string; +} + +export function SettingsGroupHeading({ + icon: Icon, + children, +}: Readonly) { + return ( + + + + {children} + + + ); +} diff --git a/src/renderer/components/settings/SystemSettings.test.tsx b/src/renderer/components/settings/SystemSettings.test.tsx index 83eb356ab..8fdcf5be8 100644 --- a/src/renderer/components/settings/SystemSettings.test.tsx +++ b/src/renderer/components/settings/SystemSettings.test.tsx @@ -6,7 +6,7 @@ import { mockSettings } from '../../__mocks__/state-mocks'; import { useSettingsStore } from '../../stores'; -import type { KeyboardAcceleratorShortcut, Percentage } from '../../types'; +import type { KeyboardAcceleratorShortcut } from '../../types'; import { SystemSettings } from './SystemSettings'; @@ -32,9 +32,6 @@ describe('renderer/components/settings/SystemSettings.tsx', () => { it.each([ ['checkbox-keyboardShortcut', 'keyboardShortcut'], - ['checkbox-showNotifications', 'showNotifications'], - ['checkbox-showUpdateNotifications', 'showUpdateNotifications'], - ['checkbox-playSound', 'playSound'], ['checkbox-openAtStartup', 'openAtStartup'], ['checkbox-keepWindowOnBlur', 'keepWindowOnBlur'], ] as const)('should toggle %s checkbox', async (testId, setting) => { @@ -211,51 +208,4 @@ describe('renderer/components/settings/SystemSettings.tsx', () => { expect(screen.queryByText('Click outside this area to cancel.')).not.toBeInTheDocument(); }); }); - - describe('playSound', () => { - it('volume controls should not be shown if playSound checkbox is false', async () => { - renderWithProviders(, { - settings: { ...mockSettings, playSound: false }, - }); - - expect(screen.getByTestId('settings-volume-group')).not.toBeVisible(); - }); - - it('volume controls should be shown if playSound checkbox is true', async () => { - renderWithProviders(, { - settings: { ...mockSettings, playSound: true }, - }); - - expect(screen.getByTestId('settings-volume-group')).toBeVisible(); - }); - - it('should increase notification volume', async () => { - renderWithProviders(); - - await userEvent.click(screen.getByTestId('settings-volume-up')); - - expect(updateSettingSpy).toHaveBeenCalledTimes(1); - expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 30); - }); - - it('should decrease notification volume', async () => { - renderWithProviders(); - - await userEvent.click(screen.getByTestId('settings-volume-down')); - - expect(updateSettingSpy).toHaveBeenCalledTimes(1); - expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 10); - }); - - it('should reset notification volume', async () => { - renderWithProviders(, { - settings: { ...mockSettings, notificationVolume: 30 as Percentage }, - }); - - await userEvent.click(screen.getByTestId('settings-volume-reset')); - - expect(updateSettingSpy).toHaveBeenCalledTimes(1); - expect(updateSettingSpy).toHaveBeenCalledWith('notificationVolume', 20); - }); - }); }); diff --git a/src/renderer/components/settings/SystemSettings.tsx b/src/renderer/components/settings/SystemSettings.tsx index e6dcbe98a..37dd6debd 100644 --- a/src/renderer/components/settings/SystemSettings.tsx +++ b/src/renderer/components/settings/SystemSettings.tsx @@ -13,24 +13,16 @@ import { import { Checkbox } from '../fields/Checkbox'; import { RadioGroup } from '../fields/RadioGroup'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; import { type KeyboardAcceleratorShortcut, OpenPreference, toLink } from '../../types'; -import { VolumeDownIcon } from '../../icons/VolumeDownIcon'; -import { VolumeUpIcon } from '../../icons/VolumeUpIcon'; import { openExternalLink } from '../../utils/system/comms'; import { formatAcceleratorForDisplay, keyboardEventToAccelerator, MODIFIER_SEGMENTS, } from '../../utils/system/keyboardShortcut'; -import { - canDecreaseVolume, - canIncreaseVolume, - decreaseVolume, - increaseVolume, -} from '../../utils/ui/volume'; const defaultSettings = DEFAULT_SETTINGS_STATE; @@ -50,12 +42,8 @@ export function SystemSettings() { const openLinks = useSettingsStore((s) => s.openLinks); const keyboardShortcut = useSettingsStore((s) => s.keyboardShortcut); const openGitifyShortcut = useSettingsStore((s) => s.openGitifyShortcut); - const showNotifications = useSettingsStore((s) => s.showNotifications); - const playSound = useSettingsStore((s) => s.playSound); - const notificationVolume = useSettingsStore((s) => s.notificationVolume); const keepWindowOnBlur = useSettingsStore((s) => s.keepWindowOnBlur); const openAtStartup = useSettingsStore((s) => s.openAtStartup); - const showUpdateNotifications = useSettingsStore((s) => s.showUpdateNotifications); const useX11Backend = useSettingsStore((s) => s.useX11Backend); const [recordingShortcut, setRecordingShortcut] = useState(false); @@ -131,11 +119,13 @@ export function SystemSettings() { return (
- System + + System + { updateSetting('openLinks', evt.target.value as OpenPreference); @@ -146,15 +136,21 @@ export function SystemSettings() { ]} tooltip={ - Controls the behavior of how external links should opened. - Foreground will open the link and bring the opened window - or browser to the front. + Controls whether opening an external link brings the destination app to the front. + + + Foreground brings the opened window or browser to the + front. Background opens the link without stealing focus from the current window. + + This applies to every link Gitify opens, including notification links opened in the + browser. + } value={openLinks} @@ -264,75 +260,9 @@ export function SystemSettings() { - toggleSetting('showNotifications')} - tooltip={ - Display native operating system notifications for new unread notifications. - } - /> - - - toggleSetting('playSound')} - /> - - - - toggleSetting('keepWindowOnBlur')} tooltip={ @@ -345,26 +275,13 @@ export function SystemSettings() { toggleSetting('openAtStartup')} tooltip={Launch {APPLICATION.NAME} automatically at startup.} visible={!window.gitify.platform.isLinux()} /> - toggleSetting('showUpdateNotifications')} - tooltip={ - - Show a notification when a {APPLICATION.NAME} update is ready. Updates will still be - checked and shown in the menu bar. - - } - /> - + {window.gitify.platform.isLinux() && ( diff --git a/src/renderer/components/settings/TraySettings.tsx b/src/renderer/components/settings/TraySettings.tsx index abfa5c7c0..ad2f92544 100644 --- a/src/renderer/components/settings/TraySettings.tsx +++ b/src/renderer/components/settings/TraySettings.tsx @@ -8,7 +8,7 @@ import { useSettingsStore } from '../../stores'; import { Checkbox } from '../fields/Checkbox'; import { RadioGroup } from '../fields/RadioGroup'; -import { Title } from '../primitives/Title'; +import { SectionHeading } from '../primitives/SectionHeading'; export function TraySettings() { const updateSetting = useSettingsStore((s) => s.updateSetting); @@ -23,7 +23,9 @@ export function TraySettings() { return (
- Tray + + Tray + should render a group heading 1`] = ` +
+
+ +
+ Fetching +
+
+
+`; diff --git a/src/renderer/routes/Settings.test.tsx b/src/renderer/routes/Settings.test.tsx index b6152c483..7b2096fd5 100644 --- a/src/renderer/routes/Settings.test.tsx +++ b/src/renderer/routes/Settings.test.tsx @@ -1,7 +1,8 @@ -import { act, screen } from '@testing-library/react'; +import { act, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { navigateMock, renderWithProviders } from '../__helpers__/test-utils'; +import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; import { SettingsRoute } from './Settings'; @@ -11,6 +12,7 @@ describe('renderer/routes/Settings.tsx', () => { it('should render itself & its children', async () => { await act(async () => { renderWithProviders(, { + accounts: [mockGitHubCloudAccount], initialEntries: ['/settings'], }); }); @@ -18,6 +20,44 @@ describe('renderer/routes/Settings.tsx', () => { expect(screen.getByTestId('settings')).toMatchSnapshot(); }); + it('should separate inbox behavior from alert delivery', async () => { + await act(async () => { + renderWithProviders(, { + accounts: [mockGitHubCloudAccount], + initialEntries: ['/settings'], + }); + }); + + const section = (name: string) => { + const fieldset = screen.getByRole('heading', { name }).closest('fieldset'); + + if (!fieldset) { + throw new Error(`Could not find the ${name} settings section`); + } + + return fieldset; + }; + + const appearance = section('Appearance'); + const inbox = section('Inbox'); + const alerts = section('Alerts'); + const system = section('System'); + + expect(within(inbox).getByTestId('checkbox-showPills')).toBeInTheDocument(); + expect(within(inbox).getByTestId('checkbox-wrapNotificationTitle')).toBeInTheDocument(); + expect(within(inbox).queryByTestId('checkbox-showNotifications')).not.toBeInTheDocument(); + expect( + within(appearance).queryByTestId('checkbox-wrapNotificationTitle'), + ).not.toBeInTheDocument(); + expect(within(alerts).getByTestId('checkbox-showNotifications')).toBeInTheDocument(); + expect(within(alerts).getByTestId('checkbox-showUpdateNotifications')).toBeInTheDocument(); + expect(within(alerts).queryByTestId('checkbox-showPills')).not.toBeInTheDocument(); + expect(within(system).queryByTestId('checkbox-showNotifications')).not.toBeInTheDocument(); + expect( + within(system).queryByTestId('checkbox-showUpdateNotifications'), + ).not.toBeInTheDocument(); + }); + it('should go back by pressing the icon', async () => { await act(async () => { renderWithProviders(, { diff --git a/src/renderer/routes/Settings.tsx b/src/renderer/routes/Settings.tsx index a3bf61be0..c54610888 100644 --- a/src/renderer/routes/Settings.tsx +++ b/src/renderer/routes/Settings.tsx @@ -4,8 +4,9 @@ import { Stack } from '@primer/react'; import { Contents } from '../components/layout/Contents'; import { Page } from '../components/layout/Page'; import { Header } from '../components/primitives/Header'; +import { AlertSettings } from '../components/settings/AlertSettings'; import { AppearanceSettings } from '../components/settings/AppearanceSettings'; -import { NotificationSettings } from '../components/settings/NotificationSettings'; +import { InboxSettings } from '../components/settings/InboxSettings'; import { SettingsFooter } from '../components/settings/SettingsFooter'; import { SettingsReset } from '../components/settings/SettingsReset'; import { SystemSettings } from '../components/settings/SystemSettings'; @@ -21,7 +22,8 @@ export function SettingsRoute() { - + + diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png index 127bb1a3c..996f908a2 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png index 0e4a03c48..ac2b2f766 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark-glass-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png index dd59b4e93..594023301 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_colorblind-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png index eff61b5cc..0f4364d16 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_dimmed-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png index d8aa37f19..7829f10e2 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-dark_tritanopia-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png index 8dc803869..c9262acec 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png index 991bbdf32..84c3aa5df 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light-glass-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png index 64abc4487..7d7141c99 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_colorblind-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png index 2bf476002..e5c84ced3 100644 Binary files a/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png and b/src/renderer/routes/__screenshots__/routes.visual.test.tsx/settings-light_tritanopia-classic-chromium-linux.png differ diff --git a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap index 756ae9378..493062459 100644 --- a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap @@ -46,7 +46,68 @@ exports[`renderer/routes/Filters.tsx > General > should render itself & its chil /> - +
+
+ +

+ Filters +

+
+
+
+
+
+
+
+ + Account +
@@ -59,11 +120,11 @@ exports[`renderer/routes/Filters.tsx > General > should render itself & its chil data-justify="start" data-padding="none" data-wrap="nowrap" - id="title-filters" + id="title-account" >

- Filters + Account

-
-
- -
-
-
-
-
- -
- +
-
+
General > should render itself & its chil
- + + Search + +
- +
General > should render itself & its chil
- + + User Type + +
- +
- +
General > should render itself & its chil
- + + Type + +
- +
- +
General > should render itself & its chil
- + + State + +
- +
- +
General > should render itself & its chil
- + + Review Request Type + +
- +
- +
General > should render itself & its chil
- + + Reason + +
- +
- +
should render itself & its children 1`] /> - +
+
+ +

+ Settings +

+
+
+
+
+
+
+
+ + Appearance +
@@ -59,11 +118,11 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] data-justify="start" data-padding="none" data-wrap="nowrap" - id="title-settings" + id="title-appearance" >

- Settings + Appearance

- -
-
-
-
-
- -
-
- -

- Appearance -

-
-
-
should render itself & its children 1`]
+
+
+
+ + Inbox + +
- - - -
-
should render itself & its children 1`] data-justify="start" data-padding="none" data-wrap="nowrap" + id="title-inbox" > - - - + Inbox +
-
-
- -
-
- -

- Notifications -

-
-
-
should render itself & its children 1`] data-wrap="nowrap" >
should render itself & its children 1`] data-padding="none" data-wrap="nowrap" > - -
- - -
-
- - -
- + Fetching +
should render itself & its children 1`] data-wrap="nowrap" >
- + + +
+ List +
should render itself & its children 1`] data-padding="none" data-wrap="nowrap" > - +
+ + +
+
+ + +
-
-
-
-
- + + +
+ GitHub +
-
-
should render itself & its children 1`] data-wrap="nowrap" >
- - +
- - + One or more accounts are missing scopes required for notification enrichment. Notification details may be incomplete. + + + Visit + + + + to review. +
+
+
should render itself & its children 1`] data-wrap="nowrap" > +
+
+
+ +
-
-
-
-
-
+ + +
+
+
+
+
+ + Alerts +
+
+ +

+ Alerts +

+
+
should render itself & its children 1`] data-padding="none" data-wrap="nowrap" > + -
- - -
-
- - -
@@ -1923,7 +1739,7 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] class="prc-ButtonGroup-Item-PqvDl" > @@ -1956,38 +1768,460 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] class="prc-ButtonGroup-Item-PqvDl" >
-
+
+ +
+ + +
+ + + +
+ +
+
+ + Tray + +
+
+ +

+ Tray +

+
+
+
+
+ + + +
+
+ + + +
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + System + +
+
+ +

+ System +

+
+
should render itself & its children 1`] data-padding="none" data-wrap="nowrap" > - +
+ + +
+
+ + +
@@ -2119,7 +2420,7 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] class="prc-ButtonGroup-Item-PqvDl" > @@ -2148,47 +2453,15 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] class="prc-ButtonGroup-Item-PqvDl" > -
-
-
-
- - - -
renders co /> - +
- + + +

+ Login to Bitbucket Cloud with API Token +

- +
renders correc /> - +
- + + +

+ Login to Gitea with Personal Access Token +

- +
renders correctly 1`] = /> - +
- + + +

+ Login with GitHub CLI +

- +
renders correctly 1`] = /> - +
- + + +

+ Login with OAuth App +

- +
renders corre /> - +
- + + +

+ Login with Personal Access Token +

- +
renders corre /> - +
- + + +

+ Login to GitLab with Personal Access Token +

- +