From e9c7a64b32bb6faa5e010a10bc7a573cf1b491cc Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 15 Jul 2024 10:04:12 -0400 Subject: [PATCH] feat(settings): open links in foreground or background (#1376) * feat(settings): open links in foreground or background * feat(settings): open links in foreground or background * Merge branch 'main' into feature/system-setting-open-preference * Merge branch 'main' into feature/system-setting-open-preference --- src/__mocks__/state-mocks.ts | 2 + src/components/AccountNotifications.test.tsx | 4 +- src/components/NotificationRow.test.tsx | 5 +- .../RepositoryNotifications.test.tsx | 4 +- src/components/Sidebar.test.tsx | 5 +- src/components/buttons/Button.test.tsx | 8 ++- .../notification/NotificationFooter.test.tsx | 9 +-- .../notification/NotificationHeader.test.tsx | 8 ++- .../settings/NotificationSettings.test.tsx | 4 +- .../settings/SettingsFooter.test.tsx | 4 +- .../settings/SystemSettings.test.tsx | 23 ++++++++ src/components/settings/SystemSettings.tsx | 14 +++++ src/context/App.test.tsx | 2 + src/context/App.tsx | 2 + src/routes/Accounts.test.tsx | 12 +++- src/routes/LoginWithOAuthApp.test.tsx | 12 ++-- .../LoginWithPersonalAccessToken.test.tsx | 13 ++-- .../__snapshots__/Settings.test.tsx.snap | 59 +++++++++++++++++++ src/types.ts | 14 ++++- src/utils/comms.test.ts | 10 +++- src/utils/comms.ts | 10 +++- src/utils/links.test.ts | 30 +++++----- 22 files changed, 202 insertions(+), 52 deletions(-) diff --git a/src/__mocks__/state-mocks.ts b/src/__mocks__/state-mocks.ts index 8498bf243..0065b2390 100644 --- a/src/__mocks__/state-mocks.ts +++ b/src/__mocks__/state-mocks.ts @@ -5,6 +5,7 @@ import { type GitifyUser, GroupBy, type Hostname, + OpenPreference, type SettingsState, Theme, type Token, @@ -88,6 +89,7 @@ const mockNotificationSettings = { }; const mockSystemSettings = { + openLinks: OpenPreference.FOREGROUND, keyboardShortcut: true, showNotificationsCountInTray: false, showNotifications: true, diff --git a/src/components/AccountNotifications.test.tsx b/src/components/AccountNotifications.test.tsx index 7f55c58f4..3b970f4b0 100644 --- a/src/components/AccountNotifications.test.tsx +++ b/src/components/AccountNotifications.test.tsx @@ -61,7 +61,9 @@ describe('components/AccountNotifications.tsx', () => { }); it('should open profile when clicked', async () => { - const openAccountProfileMock = jest.spyOn(links, 'openAccountProfile'); + const openAccountProfileMock = jest + .spyOn(links, 'openAccountProfile') + .mockImplementation(); const props = { account: mockGitHubCloudAccount, diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx index 15a6abb5b..6e480948d 100644 --- a/src/components/NotificationRow.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -13,9 +13,8 @@ import * as links from '../utils/links'; import { NotificationRow } from './NotificationRow'; describe('components/NotificationRow.tsx', () => { - beforeEach(() => { - jest.spyOn(links, 'openNotification'); - }); + jest.spyOn(links, 'openNotification'); + jest.spyOn(comms, 'openExternalLink').mockImplementation(); afterEach(() => { jest.clearAllMocks(); diff --git a/src/components/RepositoryNotifications.test.tsx b/src/components/RepositoryNotifications.test.tsx index 1e67b7ee7..4a2403338 100644 --- a/src/components/RepositoryNotifications.test.tsx +++ b/src/components/RepositoryNotifications.test.tsx @@ -37,7 +37,9 @@ describe('components/Repository.tsx', () => { }); it('should open the browser when clicking on the repo name', () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); render( diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 81e0b7b3a..7958fd2e1 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -15,8 +15,9 @@ jest.mock('react-router-dom', () => ({ describe('components/Sidebar.tsx', () => { const fetchNotifications = jest.fn(); - - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); afterEach(() => { jest.clearAllMocks(); diff --git a/src/components/buttons/Button.test.tsx b/src/components/buttons/Button.test.tsx index d83667e8e..31caf6a1e 100644 --- a/src/components/buttons/Button.test.tsx +++ b/src/components/buttons/Button.test.tsx @@ -1,11 +1,13 @@ import { MarkGithubIcon } from '@primer/octicons-react'; import { fireEvent, render, screen } from '@testing-library/react'; -import { shell } from 'electron'; import type { Link } from '../../types'; +import * as comms from '../../utils/comms'; import { Button, type IButton } from './Button'; describe('components/buttons/Button.tsx', () => { - const openExternalMock = jest.spyOn(shell, 'openExternal'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); const props: IButton = { label: 'button', @@ -39,6 +41,6 @@ describe('components/buttons/Button.tsx', () => { const buttonElement = screen.getByLabelText('button'); fireEvent.click(buttonElement); - expect(openExternalMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/components/notification/NotificationFooter.test.tsx b/src/components/notification/NotificationFooter.test.tsx index fd01cc043..970330c0d 100644 --- a/src/components/notification/NotificationFooter.test.tsx +++ b/src/components/notification/NotificationFooter.test.tsx @@ -8,14 +8,9 @@ import { GroupBy, type Link } from '../../types'; import type { UserType } from '../../typesGitHub'; import { mockSingleNotification } from '../../utils/api/__mocks__/response-mocks'; import * as comms from '../../utils/comms'; -import * as links from '../../utils/links'; import { NotificationFooter } from './NotificationFooter'; describe('components/notification/NotificationFooter.tsx', () => { - beforeEach(() => { - jest.spyOn(links, 'openNotification'); - }); - afterEach(() => { jest.clearAllMocks(); }); @@ -122,7 +117,9 @@ describe('components/notification/NotificationFooter.tsx', () => { }); it('should open notification user profile', () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); const props = { notification: { diff --git a/src/components/notification/NotificationHeader.test.tsx b/src/components/notification/NotificationHeader.test.tsx index 5216da20d..9271aaf1c 100644 --- a/src/components/notification/NotificationHeader.test.tsx +++ b/src/components/notification/NotificationHeader.test.tsx @@ -7,6 +7,10 @@ import * as comms from '../../utils/comms'; import { NotificationHeader } from './NotificationHeader'; describe('components/notification/NotificationHeader.tsx', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render itself & its children - group by repositories', async () => { const props = { notification: mockSingleNotification, @@ -38,7 +42,9 @@ describe('components/notification/NotificationHeader.tsx', () => { }); it('should open notification user profile - group by date', () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); const props = { notification: mockSingleNotification, diff --git a/src/components/settings/NotificationSettings.test.tsx b/src/components/settings/NotificationSettings.test.tsx index f1ac7ea67..6cfbd1f8a 100644 --- a/src/components/settings/NotificationSettings.test.tsx +++ b/src/components/settings/NotificationSettings.test.tsx @@ -60,7 +60,9 @@ describe('routes/components/settings/NotificationSettings.tsx', () => { }); it('should open official docs for showOnlyParticipating tooltip', async () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); await act(async () => { render( diff --git a/src/components/settings/SettingsFooter.test.tsx b/src/components/settings/SettingsFooter.test.tsx index ddd6bf31a..5f1f41786 100644 --- a/src/components/settings/SettingsFooter.test.tsx +++ b/src/components/settings/SettingsFooter.test.tsx @@ -81,7 +81,9 @@ describe('routes/components/settings/SettingsFooter.tsx', () => { ...originalEnv, NODE_ENV: 'production', }; - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); await act(async () => { render( diff --git a/src/components/settings/SystemSettings.test.tsx b/src/components/settings/SystemSettings.test.tsx index 3a192f48c..f424b79c2 100644 --- a/src/components/settings/SystemSettings.test.tsx +++ b/src/components/settings/SystemSettings.test.tsx @@ -11,6 +11,29 @@ describe('routes/components/settings/SystemSettings.tsx', () => { jest.clearAllMocks(); }); + it('should change the open links radio group', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + fireEvent.click(screen.getByLabelText('Background')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('openLinks', 'BACKGROUND'); + }); + it('should toggle the keyboardShortcut checkbox', async () => { await act(async () => { render( diff --git a/src/components/settings/SystemSettings.tsx b/src/components/settings/SystemSettings.tsx index 08ba6ca3e..1d87ad3f7 100644 --- a/src/components/settings/SystemSettings.tsx +++ b/src/components/settings/SystemSettings.tsx @@ -1,9 +1,11 @@ import { DeviceDesktopIcon } from '@primer/octicons-react'; import { type FC, useContext } from 'react'; import { AppContext } from '../../context/App'; +import type { OpenPreference } from '../../types'; import Constants from '../../utils/constants'; import { isLinux, isMacOS } from '../../utils/platform'; import { Checkbox } from '../fields/Checkbox'; +import { RadioGroup } from '../fields/RadioGroup'; import { Legend } from './Legend'; export const SystemSettings: FC = () => { @@ -12,6 +14,18 @@ export const SystemSettings: FC = () => { return (
System + { + updateSetting('openLinks', evt.target.value as OpenPreference); + }} + /> { groupBy: 'REPOSITORY', filterReasons: [], zoomPercentage: 100, + openLinks: 'FOREGROUND', } as SettingsState, }); }); @@ -440,6 +441,7 @@ describe('context/App.tsx', () => { groupBy: 'REPOSITORY', filterReasons: [], zoomPercentage: 100, + openLinks: 'FOREGROUND', } as SettingsState, }); }); diff --git a/src/context/App.tsx b/src/context/App.tsx index 2db248f75..1cca8edde 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -15,6 +15,7 @@ import { type AuthState, type GitifyError, GroupBy, + OpenPreference, type SettingsState, type SettingsValue, type Status, @@ -69,6 +70,7 @@ const defaultNotificationSettings = { }; const defaultSystemSettings = { + openLinks: OpenPreference.FOREGROUND, keyboardShortcut: true, showNotificationsCountInTray: false, showNotifications: true, diff --git a/src/routes/Accounts.test.tsx b/src/routes/Accounts.test.tsx index 573df05a9..3b4e84624 100644 --- a/src/routes/Accounts.test.tsx +++ b/src/routes/Accounts.test.tsx @@ -73,7 +73,9 @@ describe('routes/Accounts.tsx', () => { describe('Account interactions', () => { it('open profile in external browser', async () => { - const openAccountProfileMock = jest.spyOn(links, 'openAccountProfile'); + const openAccountProfileMock = jest + .spyOn(links, 'openAccountProfile') + .mockImplementation(); await act(async () => { render( @@ -101,7 +103,9 @@ describe('routes/Accounts.tsx', () => { }); it('open host in external browser', async () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); await act(async () => { render( @@ -127,7 +131,9 @@ describe('routes/Accounts.tsx', () => { }); it('open developer settings in external browser', async () => { - const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); await act(async () => { render( diff --git a/src/routes/LoginWithOAuthApp.test.tsx b/src/routes/LoginWithOAuthApp.test.tsx index 2f46a141c..6647363c9 100644 --- a/src/routes/LoginWithOAuthApp.test.tsx +++ b/src/routes/LoginWithOAuthApp.test.tsx @@ -1,8 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { shell } from 'electron'; import { MemoryRouter } from 'react-router-dom'; import { AppContext } from '../context/App'; import type { AuthState, ClientID, ClientSecret, Hostname } from '../types'; +import * as comms from '../utils/comms'; import { LoginWithOAuthApp, validate } from './LoginWithOAuthApp'; const mockNavigate = jest.fn(); @@ -12,7 +12,9 @@ jest.mock('react-router-dom', () => ({ })); describe('routes/LoginWithOAuthApp.tsx', () => { - const openExternalMock = jest.spyOn(shell, 'openExternal'); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); const mockAuth: AuthState = { accounts: [], @@ -84,7 +86,7 @@ describe('routes/LoginWithOAuthApp.tsx', () => { fireEvent.click(screen.getByText('Create new OAuth App')); - expect(openExternalMock).toHaveBeenCalledTimes(0); + expect(openExternalLinkMock).toHaveBeenCalledTimes(0); }); it('should open in browser if hostname configured', async () => { @@ -102,7 +104,7 @@ describe('routes/LoginWithOAuthApp.tsx', () => { fireEvent.click(screen.getByText('Create new OAuth App')); - expect(openExternalMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); }); }); @@ -143,6 +145,6 @@ describe('routes/LoginWithOAuthApp.tsx', () => { fireEvent.click(screen.getByLabelText('GitHub Docs')); - expect(openExternalMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/routes/LoginWithPersonalAccessToken.test.tsx b/src/routes/LoginWithPersonalAccessToken.test.tsx index def9feb25..9343f816a 100644 --- a/src/routes/LoginWithPersonalAccessToken.test.tsx +++ b/src/routes/LoginWithPersonalAccessToken.test.tsx @@ -5,9 +5,9 @@ import { screen, waitFor, } from '@testing-library/react'; -import { shell } from 'electron'; import { MemoryRouter } from 'react-router-dom'; import { AppContext } from '../context/App'; +import * as comms from '../utils/comms'; import { LoginWithPersonalAccessToken, validate, @@ -20,9 +20,10 @@ jest.mock('react-router-dom', () => ({ })); describe('routes/LoginWithPersonalAccessToken.tsx', () => { - const openExternalMock = jest.spyOn(shell, 'openExternal'); - const mockValidateToken = jest.fn(); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); afterEach(() => { jest.clearAllMocks(); @@ -88,7 +89,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { fireEvent.click(screen.getByText('Generate a PAT')); - expect(openExternalMock).toHaveBeenCalledTimes(0); + expect(openExternalLinkMock).toHaveBeenCalledTimes(0); }); it('should open in browser if hostname configured', async () => { @@ -104,7 +105,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { fireEvent.click(screen.getByText('Generate a PAT')); - expect(openExternalMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); }); }); @@ -198,6 +199,6 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => { fireEvent.click(screen.getByLabelText('GitHub Docs')); - expect(openExternalMock).toHaveBeenCalledTimes(1); + expect(openExternalLinkMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap index a2fa1103a..58ff65ece 100644 --- a/src/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -555,6 +555,65 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = ` System +
+
+
+ +
+
+
+ + +
+
+ + +
+
+
+
diff --git a/src/types.ts b/src/types.ts index fd6f0822b..a51c648e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,7 +51,13 @@ export interface Account { user: GitifyUser | null; } -export type SettingsValue = boolean | Theme | GroupBy | Reason[] | number; +export type SettingsValue = + | boolean + | number + | GroupBy + | OpenPreference + | Reason[] + | Theme; export type SettingsState = AppearanceSettingsState & NotificationSettingsState & @@ -76,6 +82,7 @@ interface NotificationSettingsState { } interface SystemSettingsState { + openLinks: OpenPreference; playSound: boolean; openAtStartup: boolean; showNotificationsCountInTray: boolean; @@ -98,6 +105,11 @@ export enum Theme { DARK = 'DARK', } +export enum OpenPreference { + FOREGROUND = 'FOREGROUND', + BACKGROUND = 'FOREGROUND', +} + export enum GroupBy { REPOSITORY = 'REPOSITORY', DATE = 'DATE', diff --git a/src/utils/comms.test.ts b/src/utils/comms.test.ts index 45ba30245..3c7402ba4 100644 --- a/src/utils/comms.test.ts +++ b/src/utils/comms.test.ts @@ -1,4 +1,5 @@ import { ipcRenderer, shell } from 'electron'; +import { mockSettings } from '../__mocks__/state-mocks'; import type { Link } from '../types'; import { getAppVersion, @@ -9,6 +10,7 @@ import { showWindow, updateTrayIcon, } from './comms'; +import * as storage from './storage'; describe('utils/comms.ts', () => { beforeEach(() => { @@ -59,9 +61,15 @@ describe('utils/comms.ts', () => { }); it('should open an external link', () => { + jest + .spyOn(storage, 'loadState') + .mockReturnValue({ settings: mockSettings }); + openExternalLink('https://www.gitify.io/' as Link); expect(shell.openExternal).toHaveBeenCalledTimes(1); - expect(shell.openExternal).toHaveBeenCalledWith('https://www.gitify.io/'); + expect(shell.openExternal).toHaveBeenCalledWith('https://www.gitify.io/', { + activate: true, + }); }); it('should ignore opening external local links file:///', () => { diff --git a/src/utils/comms.ts b/src/utils/comms.ts index 936bc4295..13cef346e 100644 --- a/src/utils/comms.ts +++ b/src/utils/comms.ts @@ -1,10 +1,16 @@ import { ipcRenderer, shell } from 'electron'; -import type { Link } from '../types'; +import { type Link, OpenPreference } from '../types'; import Constants from './constants'; +import { loadState } from './storage'; export function openExternalLink(url: Link): void { if (url.toLowerCase().startsWith('https://')) { - shell.openExternal(url); + // Load the state from local storage to avoid having to pass settings as a parameter + const { settings } = loadState(); + + shell.openExternal(url, { + activate: settings.openLinks === OpenPreference.FOREGROUND, + }); } } diff --git a/src/utils/links.test.ts b/src/utils/links.test.ts index 316fff367..ee1bb5a51 100644 --- a/src/utils/links.test.ts +++ b/src/utils/links.test.ts @@ -23,9 +23,9 @@ import { } from './links'; describe('utils/links.ts', () => { - beforeEach(() => { - jest.spyOn(comms, 'openExternalLink'); - }); + const openExternalLinkMock = jest + .spyOn(comms, 'openExternalLink') + .mockImplementation(); afterEach(() => { jest.clearAllMocks(); @@ -33,42 +33,42 @@ describe('utils/links.ts', () => { it('openGitifyRepository', () => { openGitifyRepository(); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/gitify-app/gitify', ); }); it('openGitifyReleaseNotes', () => { openGitifyReleaseNotes('v1.0.0'); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/gitify-app/gitify/releases/tag/v1.0.0', ); }); it('openGitHubNotifications', () => { openGitHubNotifications(); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/notifications', ); }); it('openGitHubIssues', () => { openGitHubIssues(); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/issues', ); }); it('openGitHubPulls', () => { openGitHubPulls(); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/pulls', ); }); it('openAccountProfile', () => { openAccountProfile(mockGitHubCloudAccount); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/octocat', ); }); @@ -76,14 +76,14 @@ describe('utils/links.ts', () => { it('openUserProfile', () => { const mockUser = partialMockUser('mock-user'); openUserProfile(mockUser); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( 'https://github.com/mock-user', ); }); it('openHost', () => { openHost('github.com' as Hostname); - expect(comms.openExternalLink).toHaveBeenCalledWith('https://github.com'); + expect(openExternalLinkMock).toHaveBeenCalledWith('https://github.com'); }); it('openDeveloperSettings', () => { @@ -93,7 +93,7 @@ describe('utils/links.ts', () => { .spyOn(authUtils, 'getDeveloperSettingsURL') .mockReturnValue(mockSettingsURL); openDeveloperSettings(mockGitHubCloudAccount); - expect(comms.openExternalLink).toHaveBeenCalledWith(mockSettingsURL); + expect(openExternalLinkMock).toHaveBeenCalledWith(mockSettingsURL); }); it('openRepository', () => { @@ -104,7 +104,7 @@ describe('utils/links.ts', () => { } as Repository; openRepository(repo); - expect(comms.openExternalLink).toHaveBeenCalledWith(mockHtmlUrl); + expect(openExternalLinkMock).toHaveBeenCalledWith(mockHtmlUrl); }); it('openNotification', async () => { @@ -113,12 +113,12 @@ describe('utils/links.ts', () => { .spyOn(helpers, 'generateGitHubWebUrl') .mockResolvedValue(mockNotificationUrl); await openNotification(mockSingleNotification); - expect(comms.openExternalLink).toHaveBeenCalledWith(mockNotificationUrl); + expect(openExternalLinkMock).toHaveBeenCalledWith(mockNotificationUrl); }); it('openParticipatingDocs', () => { openGitHubParticipatingDocs(); - expect(comms.openExternalLink).toHaveBeenCalledWith( + expect(openExternalLinkMock).toHaveBeenCalledWith( Constants.GITHUB_DOCS.PARTICIPATING_URL, ); });