From fe7c2e1a239568e7ca3efb19f20531830d51d4fe Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Tue, 15 Dec 2020 20:43:46 +0000 Subject: [PATCH 01/25] chore: Remove redux and start migrating to context --- package.json | 9 +- .../Loading.test.tsx} | 15 +- src/components/Loading.tsx | 27 ++ .../Sidebar.test.tsx} | 42 +-- src/components/Sidebar.tsx | 111 +++++++ src/context/App.tsx | 88 ++++++ src/context/Notifications.tsx | 40 +++ src/hooks/useGitHubAuth.ts | 88 ++++++ src/js/__mocks__/mockedData.ts | 2 +- src/js/__mocks__/redux-storage.js | 21 -- src/js/app.tsx | 59 ++-- .../__snapshots__/sidebar.test.tsx.snap | 279 ------------------ src/js/components/all-read.test.tsx | 2 +- src/js/components/all-read.tsx | 10 +- src/js/components/loading.tsx | 34 --- src/js/components/oops.test.tsx | 3 +- src/js/components/oops.tsx | 6 +- src/js/components/sidebar.tsx | 153 ---------- src/js/reducers/index.js | 14 - src/js/reducers/settings.test.ts | 26 -- src/js/reducers/settings.ts | 21 -- src/js/routes/login.tsx | 77 ----- src/js/routes/settings.tsx | 193 ------------ src/js/store/configureStore.test.ts | 12 - src/js/store/configureStore.ts | 50 ---- src/js/utils/comms.ts | 2 - src/js/utils/helpers.test.ts | 2 +- src/js/utils/helpers.ts | 2 +- src/js/utils/notifications.test.ts | 2 +- src/js/utils/notifications.ts | 2 +- .../login.test.tsx => routes/Login.test.tsx} | 35 +-- src/routes/Login.tsx | 56 ++++ .../LoginEnterprise.test.tsx} | 1 + .../LoginEnterprise.tsx} | 114 +++---- .../Settings.test.tsx} | 43 +-- src/routes/Settings.tsx | 145 +++++++++ .../__snapshots__/Login.test.tsx.snap} | 0 .../LoginEnterprise.test.tsx.snap} | 0 .../__snapshots__/Settings.test.tsx.snap} | 0 src/types.ts | 33 +++ src/types/actions.ts | 5 - src/types/reducers.ts | 41 --- .../constants.tsx => utils/Constants.ts} | 4 +- tsconfig.json | 9 +- yarn.lock | 140 +++------ 45 files changed, 749 insertions(+), 1269 deletions(-) rename src/{js/components/loading.test.tsx => components/Loading.test.tsx} (79%) create mode 100644 src/components/Loading.tsx rename src/{js/components/sidebar.test.tsx => components/Sidebar.test.tsx} (77%) create mode 100644 src/components/Sidebar.tsx create mode 100644 src/context/App.tsx create mode 100644 src/context/Notifications.tsx create mode 100644 src/hooks/useGitHubAuth.ts delete mode 100644 src/js/__mocks__/redux-storage.js delete mode 100644 src/js/components/__snapshots__/sidebar.test.tsx.snap delete mode 100644 src/js/components/loading.tsx delete mode 100644 src/js/components/sidebar.tsx delete mode 100644 src/js/reducers/index.js delete mode 100644 src/js/reducers/settings.test.ts delete mode 100644 src/js/reducers/settings.ts delete mode 100644 src/js/routes/login.tsx delete mode 100644 src/js/routes/settings.tsx delete mode 100644 src/js/store/configureStore.test.ts delete mode 100644 src/js/store/configureStore.ts rename src/{js/routes/login.test.tsx => routes/Login.test.tsx} (69%) create mode 100644 src/routes/Login.tsx rename src/{js/routes/enterprise-login.test.tsx => routes/LoginEnterprise.test.tsx} (99%) rename src/{js/routes/enterprise-login.tsx => routes/LoginEnterprise.tsx} (50%) rename src/{js/routes/settings.test.tsx => routes/Settings.test.tsx} (81%) create mode 100644 src/routes/Settings.tsx rename src/{js/routes/__snapshots__/login.test.tsx.snap => routes/__snapshots__/Login.test.tsx.snap} (100%) rename src/{js/routes/__snapshots__/enterprise-login.test.tsx.snap => routes/__snapshots__/LoginEnterprise.test.tsx.snap} (100%) rename src/{js/routes/__snapshots__/settings.test.tsx.snap => routes/__snapshots__/Settings.test.tsx.snap} (100%) delete mode 100644 src/types/actions.ts delete mode 100644 src/types/reducers.ts rename src/{js/utils/constants.tsx => utils/Constants.ts} (93%) diff --git a/package.json b/package.json index 29d047b9c..8e28c2c9d 100644 --- a/package.json +++ b/package.json @@ -121,14 +121,11 @@ "react-emojione": "=5.0.1", "react-final-form": "^6.4.0", "react-is": "^16.13.1", - "react-redux": "=7.2.0", - "react-router-dom": "^5.1.2", + "react-redux": "=7.2.2", + "react-router-dom": "^5.2.0", "react-transition-group": "^4.3.0", "react-typist": "^2.0.5", "redux": "=4.0.5", - "redux-storage": "=4.1.2", - "redux-storage-decorator-filter": "=1.1.8", - "redux-storage-engine-localstorage": "=1.1.4", "redux-thunk": "=2.3.0", "tailwindcss": "^2.0.2", "ts-loader": "^8.0.11", @@ -140,7 +137,7 @@ "@types/lodash": "^4.14.165", "@types/node": "^14.14.9", "@types/react": "^16.9.32", - "@types/react-redux": "^7.1.7", + "@types/react-router-dom": "^5.1.6", "@types/react-transition-group": "^4.2.4", "@types/styled-components": "^5.0.1", "css-loader": "^5.0.1", diff --git a/src/js/components/loading.test.tsx b/src/components/Loading.test.tsx similarity index 79% rename from src/js/components/loading.test.tsx rename to src/components/Loading.test.tsx index c7ad8eda0..8290bf9ee 100644 --- a/src/js/components/loading.test.tsx +++ b/src/components/Loading.test.tsx @@ -2,8 +2,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import * as NProgress from 'nprogress'; -import { AppState } from '../../types/reducers'; -import { Loading, mapStateToProps } from './loading'; +import { Loading } from './loading'; jest.mock('nprogress', () => { return { @@ -22,18 +21,6 @@ describe('components/loading.js', function () { NProgress.remove.mockReset(); }); - it('should test the mapStateToProps method', () => { - const state = { - notifications: { - isFetching: false, - }, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.isLoading).toBeFalsy(); - }); - it('should check that NProgress is getting called in getDerivedStateFromProps (loading)', function () { const { container } = render(); diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 000000000..22887abc4 --- /dev/null +++ b/src/components/Loading.tsx @@ -0,0 +1,27 @@ +import React, { useContext } from 'react'; +import * as NProgress from 'nprogress'; +import { NotificationsContext } from '../context/Notifications'; + +export const Loading = () => { + const { isFetching } = useContext(NotificationsContext); + + React.useEffect(() => { + NProgress.configure({ + showSpinner: false, + }); + + return () => { + NProgress.remove(); + }; + }, []); + + React.useEffect(() => { + if (isFetching) { + NProgress.start(); + } else { + NProgress.done(); + } + }, [isFetching]); + + return null; +}; diff --git a/src/js/components/sidebar.test.tsx b/src/components/Sidebar.test.tsx similarity index 77% rename from src/js/components/sidebar.test.tsx rename to src/components/Sidebar.test.tsx index eec589100..f53a4ea82 100644 --- a/src/js/components/sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -5,9 +5,10 @@ import { MemoryRouter } from 'react-router-dom'; const { shell, ipcRenderer } = require('electron'); -import { Sidebar, mapStateToProps } from './sidebar'; -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; -import { AuthState, AppState } from '../../types/reducers'; +import { AuthState } from '../types'; +import { mapStateToProps } from '../js/components/loading'; +import { mockedEnterpriseAccounts } from '../js/__mocks__/mockedData'; +import { Sidebar } from './sidebar'; describe('components/Sidebar.tsx', () => { let clock; @@ -42,41 +43,6 @@ describe('components/Sidebar.tsx', () => { clock.clearAllTimers(); }); - describe('mapStateToProps', () => { - const state = { - auth: { - token: '12345', - enterpriseAccounts: mockedEnterpriseAccounts, - } as AuthState, - notifications: { - response: [{ hostname: 'Dolores', notifications: [{}, {}] }], - }, - } as AppState; - - it('should accept a provided token', () => { - const mappedProps = mapStateToProps(state); - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - expect(mappedProps.connectedAccounts).toBe(2); - }); - - it('should count notification lengths', () => { - const mappedProps = mapStateToProps(state); - expect(mappedProps.notificationsCount).toBe(2); - }); - - it('should accept a null token', () => { - const mappedProps = mapStateToProps({ - ...state, - auth: { - ...state.auth, - token: null, - }, - }); - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - expect(mappedProps.connectedAccounts).toBe(1); - }); - }); - it('should render itself & its children (logged in)', () => { const tree = TestRenderer.create( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 000000000..6ce318fbe --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,111 @@ +import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import { shell } from 'electron'; +import { useLocation } from 'react-router-dom'; +import * as Octicons from '@primer/octicons-react'; +import { useHistory } from 'react-router-dom'; + +import { AppContext } from '../context/App'; +import { Constants } from '../utils/Constants'; +import { Logo } from '../js/components/ui/logo'; +import { NotificationsContext } from '../context/Notifications'; + +export const Sidebar: React.FC = () => { + const history = useHistory(); + const location = useLocation(); + + const { accounts, isLoggedIn } = useContext(AppContext); + const { notifications, fetchNotifications } = useContext( + NotificationsContext + ); + + useEffect(() => { + const iFrequency = 60000; + + const requestInterval = setInterval(() => { + refreshNotifications(); + }, iFrequency); + + return () => { + clearInterval(requestInterval); + }; + }, []); + + useEffect(() => { + fetchNotifications(); + }, [accounts]); + + const refreshNotifications = useCallback(() => { + if (isLoggedIn) { + fetchNotifications(); + } + }, [isLoggedIn]); + + const onOpenBrowser = useCallback(() => { + shell.openExternal(`https://github.com/${Constants.REPO_SLUG}`); + }, []); + + const goToSettings = useCallback(() => { + if (location.pathname === '/settings') { + return history.goBack(); + } + return history.push('/settings'); + }, []); + + const notificationsCount = useMemo(() => { + return notifications.reduce( + (memo, account) => memo + account.notifications.length, + 0 + ); + }, [notifications]); + + const footerButtonClasses = + 'flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none'; + + return ( +
+
+ + + {notificationsCount > 0 && ( +
+ + {notificationsCount} +
+ )} +
+ +
+ {isLoggedIn && ( + <> + + + + + )} + +
+ +
+
+
+ ); +}; diff --git a/src/context/App.tsx b/src/context/App.tsx new file mode 100644 index 000000000..8a2ec95e7 --- /dev/null +++ b/src/context/App.tsx @@ -0,0 +1,88 @@ +import React, { + useState, + createContext, + useCallback, + useEffect, + useMemo, +} from 'react'; +import { useGitHubAuth } from '../hooks/useGitHubAuth'; + +import { Appearance, AuthState, SettingsState } from '../types'; + +const defaultAccounts: AuthState = { + token: null, + enterpriseAccounts: [], +}; + +const defaultSettings: SettingsState = { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false, + appearance: Appearance.SYSTEM, +}; + +interface AppContextState { + accounts: AuthState; + isLoggedIn: boolean; + login: () => void; + logout: () => void; + + settings: SettingsState; + updateSetting: (name: keyof SettingsState, value: any) => void; +} + +export const AppContext = createContext>({}); + +export const AppProvider = ({ children }: { children: React.ReactNode }) => { + const [accounts, setAccounts] = useState(defaultAccounts); + const [settings, setSettings] = useState(defaultSettings); + const { authGitHub, getToken } = useGitHubAuth(); + + const updateSetting = useCallback((name: keyof SettingsState, value: any) => { + setSettings({ ...settings, [name]: value }); + }, []); + + const isLoggedIn = useMemo(() => { + return !!accounts.token || accounts.enterpriseAccounts.length > 0; + }, [accounts]); + + const login = useCallback(async () => { + const authCode = await authGitHub(); + const { token } = await getToken(authCode.code); + setAccounts({ ...accounts, token }); + }, []); + + const logout = useCallback(() => { + setAccounts(defaultAccounts); + }, []); + + useEffect(() => { + if (!accounts.token && !accounts.enterpriseAccounts) { + // Empty local storage + } else { + // Save accounts to local storage + } + }, [accounts]); + + useEffect(() => { + // Reload settings + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/context/Notifications.tsx b/src/context/Notifications.tsx new file mode 100644 index 000000000..235f568d3 --- /dev/null +++ b/src/context/Notifications.tsx @@ -0,0 +1,40 @@ +import React, { useState, createContext, useCallback } from 'react'; + +import { AccountNotifications } from '../types'; + +interface NotificationsContextState { + notifications: AccountNotifications[]; + fetchNotifications: () => Promise; + isFetching: boolean; +} + +export const NotificationsContext = createContext< + Partial +>({}); + +export const NotificationsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [isFetching, setIsFetching] = useState(false); + const [notifications, setNotifications] = useState( + [] + ); + + const fetchNotifications = useCallback(async () => { + // + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts new file mode 100644 index 000000000..37e086fe2 --- /dev/null +++ b/src/hooks/useGitHubAuth.ts @@ -0,0 +1,88 @@ +const { remote } = require('electron'); +const BrowserWindow = remote.BrowserWindow; + +import { apiRequest } from '../js/utils/api-requests'; +import { AuthResponse, AuthTokenResponse } from '../types'; +import { Constants } from '../utils/Constants'; + +export const useGitHubAuth = (authOptions = Constants.DEFAULT_AUTH_OPTIONS) => { + const { hostname } = authOptions; + + const authGitHub = (): Promise => { + return new Promise((resolve, reject) => { + // Build the OAuth consent page URL + const authWindow = new BrowserWindow({ + width: 800, + height: 600, + show: true, + }); + + const githubUrl = `https://${hostname}/login/oauth/authorize`; + const authUrl = `${githubUrl}?client_id=${authOptions.clientId}&scope=${Constants.AUTH_SCOPE}`; + + const session = authWindow.webContents.session; + session.clearStorageData(); + + authWindow.loadURL(authUrl); + + const handleCallback = (url: string) => { + const raw_code = /code=([^&]*)/.exec(url) || null; + const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; + const error = /\?error=(.+)$/.exec(url); + if (code || error) { + // Close the browser if code found or error + authWindow.destroy(); + } + // If there is a code, proceed to get token from github + if (code) { + resolve({ hostname, code }); + } else if (error) { + reject( + "Oops! Something went wrong and we couldn't " + + 'log you in using Github. Please try again.' + ); + } + }; + + // If "Done" button is pressed, hide "Loading" + authWindow.on('close', () => { + authWindow.destroy(); + }); + + authWindow.webContents.on( + 'did-fail-load', + (event, errorCode, errorDescription, validatedURL) => { + if (validatedURL.includes(hostname)) { + authWindow.destroy(); + reject(`Invalid Hostname. Could not load https://${hostname}/.`); + } + } + ); + + authWindow.webContents.on('will-redirect', (event, url) => { + event.preventDefault(); + handleCallback(url); + }); + }); + }; + + const getToken = async ( + code: string, + authOptions = Constants.DEFAULT_AUTH_OPTIONS + ): Promise => { + const url = `https://${authOptions.hostname}/login/oauth/access_token`; + const data = { + client_id: authOptions.clientId, + client_secret: authOptions.clientSecret, + code: code, + }; + + const response = await apiRequest(url, 'POST', data); + return { + hostname, + token: response.data.access_token, + }; + }; + + return { authGitHub, getToken }; +}; diff --git a/src/js/__mocks__/mockedData.ts b/src/js/__mocks__/mockedData.ts index 8a4391ab4..f0c98522b 100644 --- a/src/js/__mocks__/mockedData.ts +++ b/src/js/__mocks__/mockedData.ts @@ -1,5 +1,5 @@ +import { EnterpriseAccount } from '../../types'; import { Notification, Repository } from '../../types/github'; -import { EnterpriseAccount } from '../../types/reducers'; export const mockedEnterpriseAccounts: EnterpriseAccount[] = [ { diff --git a/src/js/__mocks__/redux-storage.js b/src/js/__mocks__/redux-storage.js deleted file mode 100644 index 1c7230be2..000000000 --- a/src/js/__mocks__/redux-storage.js +++ /dev/null @@ -1,21 +0,0 @@ -export function createMiddleware() { - return () => { - return (next) => (action) => { - return next(action); - }; - }; -} - -export function createLoader() { - const state = { - auth: { - token: 'LOGGED_IN_TOKEN', - }, - }; - - return () => new Promise((resolve) => resolve(state)); -} - -export function reducer(clb) { - return (state, action) => clb(state, action); -} diff --git a/src/js/app.tsx b/src/js/app.tsx index f6223fb53..e451910dc 100644 --- a/src/js/app.tsx +++ b/src/js/app.tsx @@ -1,35 +1,28 @@ -import * as React from 'react'; +import React, { useContext } from 'react'; import { Redirect, HashRouter as Router, Route, Switch, } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import configureStore from './store/configureStore'; -import Loading from './components/loading'; -import Sidebar from './components/sidebar'; - -import EnterpriseLoginRoute from './routes/enterprise-login'; -import LoginRoute from './routes/login'; -import NotificationsRoute from './routes/notifications'; -import SettingsRoute from './routes/settings'; - -// Store -export const store = configureStore(); +import { AppContext, AppProvider } from '../context/App'; +import { Loading } from '../components/Loading'; +import { LoginEnterpriseRoute } from '../routes/LoginEnterprise'; +import { LoginRoute } from '../routes/Login'; +import { NotificationsProvider } from '../context/Notifications'; +import { NotificationsRoute } from './routes/notifications'; +import { SettingsRoute } from '../routes/Settings'; +import { Sidebar } from '../components/Sidebar'; export const PrivateRoute = ({ component: Component, ...rest }) => { - // @ts-ignore - const authReducer = store.getState().auth; - const isAuthenticated = - authReducer.token !== null || authReducer.enterpriseAccounts.length > 0; + const { isLoggedIn } = useContext(AppContext); return ( - isAuthenticated ? ( + isLoggedIn ? ( ) : ( { export const App = () => { return ( - - -
- - + + + +
+ + - - - - - - -
-
- + + + + + + +
+
+ + ); }; diff --git a/src/js/components/__snapshots__/sidebar.test.tsx.snap b/src/js/components/__snapshots__/sidebar.test.tsx.snap deleted file mode 100644 index 90a4a5cf4..000000000 --- a/src/js/components/__snapshots__/sidebar.test.tsx.snap +++ /dev/null @@ -1,279 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/Sidebar.tsx should render itself & its children (logged in) 1`] = ` -
-
- - - - - - - - - - - -
-
-
-
- - -
-
-
-
-`; - -exports[`components/Sidebar.tsx should render itself & its children (logged out) 1`] = ` -
-
- - - - - - - - - - - -
-
-
-
-
-
-
-
-`; diff --git a/src/js/components/all-read.test.tsx b/src/js/components/all-read.test.tsx index 6af7247e5..37d5997dd 100644 --- a/src/js/components/all-read.test.tsx +++ b/src/js/components/all-read.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; +import { Constants } from '../../utils/Constants'; -import Constants from '../utils/constants'; import { AllRead } from './all-read'; jest.mock('react-typist'); diff --git a/src/js/components/all-read.tsx b/src/js/components/all-read.tsx index a6b28fed7..45e59fc9f 100644 --- a/src/js/components/all-read.tsx +++ b/src/js/components/all-read.tsx @@ -2,21 +2,21 @@ import * as React from 'react'; import Typist from 'react-typist'; import { emojify } from 'react-emojione'; -import constants from '../utils/constants'; +import { Constants } from '../../utils/Constants'; export const AllRead = () => { const message = React.useMemo( () => - constants.ALLREAD_MESSAGES[ - Math.floor(Math.random() * constants.ALLREAD_MESSAGES.length) + Constants.ALLREAD_MESSAGES[ + Math.floor(Math.random() * Constants.ALLREAD_MESSAGES.length) ], [] ); const emoji = React.useMemo( () => - constants.ALLREAD_EMOJIS[ - Math.floor(Math.random() * constants.ALLREAD_EMOJIS.length) + Constants.ALLREAD_EMOJIS[ + Math.floor(Math.random() * Constants.ALLREAD_EMOJIS.length) ], [] ); diff --git a/src/js/components/loading.tsx b/src/js/components/loading.tsx deleted file mode 100644 index b377145c8..000000000 --- a/src/js/components/loading.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import * as NProgress from 'nprogress'; -import { AppState } from '../../types/reducers'; -import { connect } from 'react-redux'; - -export const Loading = ({ isLoading }: { isLoading: boolean }) => { - React.useEffect(() => { - NProgress.configure({ - showSpinner: false, - }); - - return () => { - NProgress.remove(); - }; - }, []); - - React.useEffect(() => { - if (isLoading) { - NProgress.start(); - } else { - NProgress.done(); - } - }, [isLoading]); - - return null; -}; - -export function mapStateToProps(state: AppState) { - return { - isLoading: state.notifications.isFetching, - }; -} - -export default connect(mapStateToProps, null)(Loading); diff --git a/src/js/components/oops.test.tsx b/src/js/components/oops.test.tsx index 70edfbf25..57d36bc26 100644 --- a/src/js/components/oops.test.tsx +++ b/src/js/components/oops.test.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; -import Constants from '../utils/constants'; +import { Constants } from '../../utils/Constants'; + import { Oops } from './oops'; describe('components/oops.tsx', function () { diff --git a/src/js/components/oops.tsx b/src/js/components/oops.tsx index 2c1eebfdc..ceb4982be 100644 --- a/src/js/components/oops.tsx +++ b/src/js/components/oops.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { emojify } from 'react-emojione'; -import constants from '../utils/constants'; +import { Constants } from '../../utils/Constants'; export const Oops = () => { const emoji = React.useMemo( () => - constants.ERROR_EMOJIS[ - Math.floor(Math.random() * constants.ERROR_EMOJIS.length) + Constants.ERROR_EMOJIS[ + Math.floor(Math.random() * Constants.ERROR_EMOJIS.length) ], [] ); diff --git a/src/js/components/sidebar.tsx b/src/js/components/sidebar.tsx deleted file mode 100644 index 0a5ce0b65..000000000 --- a/src/js/components/sidebar.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import * as React from 'react'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import { shell } from 'electron'; -import * as Octicons from '@primer/octicons-react'; - -import { AppState } from '../../types/reducers'; -import { fetchNotifications, logout } from '../actions'; -import { isUserEitherLoggedIn } from '../utils/helpers'; -import { Logo } from './ui/logo'; -import Constants from '../utils/constants'; - -interface IProps { - fetchNotifications: () => void; - - connectedAccounts: number; - notificationsCount: number; - isEitherLoggedIn: boolean; - - history: any; - location: any; -} - -export class Sidebar extends React.Component { - requestInterval: any; - - componentDidMount() { - const self = this; - const iFrequency = 60000; - - this.requestInterval = setInterval(() => { - self.refreshNotifications(); - }, iFrequency); - } - - state = { - connectedAccounts: [], - }; - - static getDerivedStateFromProps(props, state) { - if (props.connectedAccounts > state.connectedAccounts) { - props.fetchNotifications(); - } - - return { - connectedAccounts: props.connectedAccounts, - }; - } - - componentWillUnmount() { - clearInterval(this.requestInterval); - } - - refreshNotifications() { - if (this.props.isEitherLoggedIn) { - this.props.fetchNotifications(); - } - } - - onOpenBrowser() { - shell.openExternal(`https://github.com/${Constants.REPO_SLUG}`); - } - - goToSettings() { - if (this.props.location.pathname === '/settings') { - return this.props.history.goBack(); - } - return this.props.history.push('/settings'); - } - - render() { - const { isEitherLoggedIn, notificationsCount } = this.props; - - const footerButtonClasses = - 'flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none'; - - return ( -
-
- - - {notificationsCount > 0 && ( -
- - {notificationsCount} -
- )} -
- -
- {isEitherLoggedIn && ( - <> - - - - - )} - -
- -
-
-
- ); - } -} - -export function mapStateToProps(state: AppState) { - const enterpriseAccounts = state.auth.enterpriseAccounts; - const isGitHubLoggedIn = state.auth.token !== null; - const connectedAccounts = isGitHubLoggedIn - ? enterpriseAccounts.length + 1 - : enterpriseAccounts.length; - - const notificationsCount = state.notifications.response.reduce( - (memo, account) => memo + account.notifications.length, - 0 - ); - - return { - isEitherLoggedIn: isUserEitherLoggedIn(state.auth), - notificationsCount, - connectedAccounts, - }; -} - -export default compose( - withRouter, - connect(mapStateToProps, { - fetchNotifications, - logout, - }) -)(Sidebar) as any; diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js deleted file mode 100644 index 6e3911f31..000000000 --- a/src/js/reducers/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import { combineReducers } from 'redux'; -import * as storage from 'redux-storage'; - -import auth from './auth'; -import notifications from './notifications'; -import settings from './settings'; - -export default storage.reducer( - combineReducers({ - auth, - notifications, - settings, - }) -); diff --git a/src/js/reducers/settings.test.ts b/src/js/reducers/settings.test.ts deleted file mode 100644 index bd188e243..000000000 --- a/src/js/reducers/settings.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import reducer from './settings'; -import { UPDATE_SETTING } from '../actions'; - -describe('reducers/settings.ts', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - }); - - it('should handle UPDATE_SETTING', () => { - const actionParticipating = { - type: UPDATE_SETTING, - setting: 'participating', - value: true, - }; - - expect(reducer(undefined, actionParticipating)).toMatchSnapshot(); - - const actionOpenAtStartUp = { - type: UPDATE_SETTING, - setting: 'openAtStartup', - value: true, - }; - - expect(reducer(undefined, actionOpenAtStartUp)).toMatchSnapshot(); - }); -}); diff --git a/src/js/reducers/settings.ts b/src/js/reducers/settings.ts deleted file mode 100644 index 474d0ef77..000000000 --- a/src/js/reducers/settings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UPDATE_SETTING } from '../actions'; -import { SettingsState } from '../../types/reducers'; -import { Appearance } from '../../types'; - -const initialState: SettingsState = { - participating: false, - playSound: true, - showNotifications: true, - markOnClick: false, - openAtStartup: false, - appearance: Appearance.SYSTEM, -}; - -export default function reducer(state = initialState, action): SettingsState { - switch (action.type) { - case UPDATE_SETTING: - return { ...state, [action.setting]: action.value }; - default: - return state; - } -} diff --git a/src/js/routes/login.tsx b/src/js/routes/login.tsx deleted file mode 100644 index bc791bb2b..000000000 --- a/src/js/routes/login.tsx +++ /dev/null @@ -1,77 +0,0 @@ -const { ipcRenderer } = require('electron'); - -import * as React from 'react'; -import { Redirect } from 'react-router-dom'; -import { connect } from 'react-redux'; - -import { AppState } from '../../types/reducers'; -import { authGithub, isUserEitherLoggedIn } from '../utils/helpers'; -import { Logo } from '../components/ui/logo'; - -interface IProps { - isEitherLoggedIn: boolean; - dispatch: any; - history: any; -} - -export class LoginPage extends React.Component { - constructor(props: IProps) { - super(props); - - this.state = { - isEitherLoggedIn: props.isEitherLoggedIn, - }; - } - - static getDerivedStateFromProps(props, state) { - if (props.isEitherLoggedIn) { - ipcRenderer.send('reopen-window'); - } - return { - isEitherLoggedIn: props.isEitherLoggedIn, - }; - } - - render() { - if (this.props.isEitherLoggedIn) { - return ; - } - - const loginButtonClass = - 'w-48 py-2 my-2 bg-gray-300 font-semibold rounded text-xs text-center dark:text-black hover:bg-gray-500 hover:text-white focus:outline-none'; - - return ( -
- - -
- GitHub Notifications
on your menu bar. -
- - - - -
- ); - } -} - -export function mapStateToProps(state: AppState) { - return { - isEitherLoggedIn: isUserEitherLoggedIn(state.auth), - }; -} - -export default connect(mapStateToProps)(LoginPage); diff --git a/src/js/routes/settings.tsx b/src/js/routes/settings.tsx deleted file mode 100644 index 77b4ab2d5..000000000 --- a/src/js/routes/settings.tsx +++ /dev/null @@ -1,193 +0,0 @@ -const { ipcRenderer, remote } = require('electron'); - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { ArrowLeftIcon } from '@primer/octicons-react'; - -import { Appearance } from '../../types'; -import { AppState, SettingsState } from '../../types/reducers'; -import { fetchNotifications, updateSetting, logout } from '../actions'; -import { FieldCheckbox } from '../components/ui/checkbox'; -import { FieldRadioGroup } from '../components/fields/radiogroup'; -import { IconAddAccount } from '../../icons/AddAccount'; -import { IconLogOut } from '../../icons/Logout'; -import { IconQuit } from '../../icons/Quit'; -import { updateTrayIcon } from '../utils/comms'; - -const isLinux = remote.process.platform === 'linux'; - -interface IProps { - hasMultipleAccounts: boolean; - fetchNotifications: () => any; - logout: () => any; - settings: SettingsState; - updateSetting: any; - history: any; -} - -export class SettingsRoute extends React.Component { - constructor(props) { - super(props); - - this.state = { - participating: props.settings.participating, - }; - } - - static getDerivedStateFromProps(props, state) { - if (props.settings.participating !== state.participating) { - props.fetchNotifications(); - } - - return { - participating: props.settings.participating, - }; - } - - logout() { - this.props.logout(); - this.props.history.goBack(); - updateTrayIcon(); - } - - quitApp() { - ipcRenderer.send('app-quit'); - } - - goToEnterprise() { - return this.props.history.replace('/enterpriselogin'); - } - - render() { - const { settings } = this.props; - - const footerButtonClass = - 'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none'; - - return ( -
-
- - -

Settings

-
- -
- { - this.props.updateSetting('appearance', evt.target.value); - }} - /> - - - this.props.updateSetting('participating', evt.target.checked) - } - /> - - this.props.updateSetting('playSound', evt.target.checked) - } - /> - - this.props.updateSetting('showNotifications', evt.target.checked) - } - /> - - this.props.updateSetting('markOnClick', evt.target.checked) - } - /> - {!isLinux && ( - - this.props.updateSetting('openAtStartup', evt.target.checked) - } - /> - )} -
- -
- - Gitify v{remote.app.getVersion()} - - -
- - - - - -
-
-
- ); - } -} - -export function mapStateToProps(state: AppState) { - const enterpriseAccounts = state.auth.enterpriseAccounts; - const isGitHubLoggedIn = state.auth.token !== null; - const hasMultipleAccounts = isGitHubLoggedIn - ? enterpriseAccounts.length > 0 - : enterpriseAccounts.length > 1; - - return { - settings: state.settings, - hasMultipleAccounts, - }; -} - -export default connect(mapStateToProps, { - updateSetting, - fetchNotifications, - logout, -})(SettingsRoute); diff --git a/src/js/store/configureStore.test.ts b/src/js/store/configureStore.test.ts deleted file mode 100644 index 402a35aa0..000000000 --- a/src/js/store/configureStore.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import configureStore from './configureStore'; - -describe('store/configureStore.js', function () { - it('should load the store', function () { - const store = configureStore(); - - expect(store.dispatch).toBeDefined(); - expect(store.subscribe).toBeDefined(); - expect(store.getState).toBeDefined(); - expect(store.replaceReducer).toBeDefined(); - }); -}); diff --git a/src/js/store/configureStore.ts b/src/js/store/configureStore.ts deleted file mode 100644 index 53c738648..000000000 --- a/src/js/store/configureStore.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunkMiddleware from 'redux-thunk'; - -import * as storage from 'redux-storage'; -import createEngine from 'redux-storage-engine-localstorage'; -import filter from 'redux-storage-decorator-filter'; - -import { UPDATE_SETTING, LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import constants from '../utils/constants'; -import notificationsMiddlware from '../middleware/notifications'; -import rootReducer from '../reducers'; -import settingsMiddleware from '../middleware/settings'; -import { setAppearance } from '../utils/appearance'; - -export default function configureStore() { - const engine = filter(createEngine(constants.STORAGE_KEY), [ - 'settings', - ['auth', 'token'], - ['auth', 'enterpriseAccounts'], - ]); - - const storageMiddleware = storage.createMiddleware( - engine, - [], - [UPDATE_SETTING, LOGIN.SUCCESS, LOGOUT] - ); - - const middlewares = [ - thunkMiddleware, - notificationsMiddlware, - settingsMiddleware, - storageMiddleware, - ]; - - let store = createStore( - rootReducer, - undefined, - applyMiddleware(...middlewares) - ); - - // Load settings from localStorage - const load = storage.createLoader(engine); - load(store).then((state) => { - const { appearance } = state.settings; - setAppearance(appearance); - }); - - return store; -} diff --git a/src/js/utils/comms.ts b/src/js/utils/comms.ts index 82be8ceea..80fb34554 100644 --- a/src/js/utils/comms.ts +++ b/src/js/utils/comms.ts @@ -1,7 +1,5 @@ const { ipcRenderer, remote, shell } = require('electron'); -import { SettingsState } from '../../types/reducers'; - export function openExternalLink(url) { shell.openExternal(url); } diff --git a/src/js/utils/helpers.test.ts b/src/js/utils/helpers.test.ts index 41f19ba28..61a8d4b88 100644 --- a/src/js/utils/helpers.test.ts +++ b/src/js/utils/helpers.test.ts @@ -2,13 +2,13 @@ const { remote } = require('electron'); const BrowserWindow = remote.BrowserWindow; const dialog = remote.dialog; +import { AuthState, EnterpriseAccount } from '../../types'; import { authGithub, generateGitHubWebUrl, generateGitHubAPIUrl, isUserEitherLoggedIn, } from './helpers'; -import { AuthState, EnterpriseAccount } from '../../types/reducers'; describe('utils/helpers.ts', () => { it('should generate the GitHub url - non enterprise - (issue)', () => { diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index fac12063e..685649304 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -5,7 +5,7 @@ const dialog = remote.dialog; import Constants from './constants'; import { loginUser } from '../actions'; -import { AuthState } from '../../types/reducers'; +import { AuthState } from '../../types'; export function getEnterpriseAccountToken(hostname, accounts): string { return accounts.find((obj) => obj.hostname === hostname).token; diff --git a/src/js/utils/notifications.test.ts b/src/js/utils/notifications.test.ts index beeaa4bfa..79ad2828b 100644 --- a/src/js/utils/notifications.test.ts +++ b/src/js/utils/notifications.test.ts @@ -2,9 +2,9 @@ import * as _ from 'lodash'; import { generateGitHubWebUrl } from '../utils/helpers'; import { mockedGithubNotifications } from '../__mocks__/mockedData'; -import { SettingsState } from '../../types/reducers'; import * as comms from './comms'; import NotificationsUtils from '../utils/notifications'; +import { SettingsState } from '../../types'; describe('utils/notifications.ts', () => { it('should raise a notification (settings - on)', () => { diff --git a/src/js/utils/notifications.ts b/src/js/utils/notifications.ts index b043a8d0f..36878f8d0 100644 --- a/src/js/utils/notifications.ts +++ b/src/js/utils/notifications.ts @@ -3,7 +3,7 @@ const { remote } = require('electron'); import { generateGitHubWebUrl } from '../utils/helpers'; import { reOpenWindow, openExternalLink } from '../utils/comms'; import { Notification } from '../../types/github'; -import { SettingsState } from '../../types/reducers'; +import { SettingsState } from '../../types'; export default { setup( diff --git a/src/js/routes/login.test.tsx b/src/routes/Login.test.tsx similarity index 69% rename from src/js/routes/login.test.tsx rename to src/routes/Login.test.tsx index 59c5d58ce..12d6bfc87 100644 --- a/src/js/routes/login.test.tsx +++ b/src/routes/Login.test.tsx @@ -1,16 +1,14 @@ +// @ts-nocheck import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router'; -import { render, fireEvent, getByLabelText } from '@testing-library/react'; - -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; +import { render, fireEvent } from '@testing-library/react'; const { ipcRenderer, remote } = require('electron'); const BrowserWindow = remote.BrowserWindow; -import { AppState } from '../../types/reducers'; -import { LoginPage, mapStateToProps } from './login'; -import * as helpers from '../utils/helpers'; +import { LoginRoute } from './login'; +import * as helpers from '../js/utils/helpers'; describe('routes/login.tsx', () => { const props = { @@ -30,21 +28,6 @@ describe('routes/login.tsx', () => { props.history.push.mockReset(); }); - it('should test the mapStateToProps method', () => { - const state = { - auth: { - token: '123456', - failed: false, - isFetching: false, - enterpriseAccounts: mockedEnterpriseAccounts, - }, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - }); - it('should render itself & its children', () => { const caseProps = { ...props, @@ -52,7 +35,7 @@ describe('routes/login.tsx', () => { const tree = TestRenderer.create( - + ); @@ -66,13 +49,13 @@ describe('routes/login.tsx', () => { const { rerender } = render( - + ); rerender( - + ); @@ -88,7 +71,7 @@ describe('routes/login.tsx', () => { const { getByLabelText } = render( - + ); @@ -100,7 +83,7 @@ describe('routes/login.tsx', () => { it('should navigate to login with github enterprise', () => { const { getByLabelText } = render( - + ); diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx new file mode 100644 index 000000000..a3d014012 --- /dev/null +++ b/src/routes/Login.tsx @@ -0,0 +1,56 @@ +const { ipcRenderer } = require('electron'); + +import React, { useCallback, useContext, useEffect } from 'react'; +import { RouteComponentProps, useHistory } from 'react-router-dom'; + +import { AppContext } from '../context/App'; +import { Logo } from '../js/components/ui/logo'; + +export const LoginRoute: React.FC = (props) => { + const history = useHistory(); + const { isLoggedIn, login } = useContext(AppContext); + + useEffect(() => { + if (isLoggedIn) { + ipcRenderer.send('reopen-window'); + history.replace('/'); + } + }, [isLoggedIn]); + + const loginUser = useCallback(async () => { + try { + await login(); + } catch (err) { + console.log('ERR:', err); + } + }, []); + + const loginButtonClass = + 'w-48 py-2 my-2 bg-gray-300 font-semibold rounded text-xs text-center dark:text-black hover:bg-gray-500 hover:text-white focus:outline-none'; + + return ( +
+ + +
+ GitHub Notifications
on your menu bar. +
+ + + + +
+ ); +}; diff --git a/src/js/routes/enterprise-login.test.tsx b/src/routes/LoginEnterprise.test.tsx similarity index 99% rename from src/js/routes/enterprise-login.test.tsx rename to src/routes/LoginEnterprise.test.tsx index af0207624..e78853685 100644 --- a/src/js/routes/enterprise-login.test.tsx +++ b/src/routes/LoginEnterprise.test.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; import { fireEvent, render } from '@testing-library/react'; diff --git a/src/js/routes/enterprise-login.tsx b/src/routes/LoginEnterprise.tsx similarity index 50% rename from src/js/routes/enterprise-login.tsx rename to src/routes/LoginEnterprise.tsx index 805de5d57..eae979aba 100644 --- a/src/js/routes/enterprise-login.tsx +++ b/src/routes/LoginEnterprise.tsx @@ -1,13 +1,12 @@ +// @ts-nocheck const ipcRenderer = require('electron').ipcRenderer; -import * as React from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { Form, FormRenderProps } from 'react-final-form'; -import { connect } from 'react-redux'; import { ArrowLeftIcon } from '@primer/octicons-react'; -import { AppState } from '../../types/reducers'; -import { authGithub } from '../utils/helpers'; -import { FieldInput } from '../components/fields/input'; +import { AppContext } from '../context/App'; +import { FieldInput } from '../js/components/fields/input'; interface IValues { hostname?: string; @@ -56,27 +55,18 @@ interface IProps { enterpriseAccountsCount: number; } -interface IState { - enterpriseAccountsCount: number; -} - -export class EnterpriseLogin extends React.Component { - state = { - enterpriseAccountsCount: 0, - }; - - static getDerivedStateFromProps(props: IProps, state) { - if (props.enterpriseAccountsCount > state.enterpriseAccountsCount) { - ipcRenderer.send('reopen-window'); - props.history.goBack(); - } +export const LoginEnterpriseRoute: React.FC = () => { + const { + accounts: { enterpriseAccounts }, + login, + } = useContext(AppContext); - return { - enterpriseAccountsCount: props.enterpriseAccountsCount, - }; - } + const enterpriseAccountsCount = useMemo(() => { + ipcRenderer.send('reopen-window'); + props.history.goBack(); + }, [enterpriseAccounts]); - renderForm = (formProps: FormRenderProps) => { + const renderForm = (formProps: FormRenderProps) => { const { handleSubmit, submitting, pristine } = formProps; return ( @@ -107,49 +97,39 @@ export class EnterpriseLogin extends React.Component { ); }; - handleSubmit(data, dispatch) { - authGithub(data, dispatch); - } + const loginEnterprise = useCallback(async (data) => { + const thing = await authGitHub(data); + console.log('RESULT ENTERPRISE:', thing); + return thing; + }, []); - render() { - return ( -
-
- - -

- Login with GitHub Enterprise -

-
- -
-
this.handleSubmit(data, this.props.dispatch)} - validate={validate} - > - {this.renderForm} -
-
-
- ); - } -} + return ( +
+
+ -export function mapStateToProps(state: AppState) { - return { - enterpriseAccountsCount: state.auth.enterpriseAccounts.length, - }; -} +

Login with GitHub Enterprise

+
-export default connect(mapStateToProps, null)(EnterpriseLogin); +
+
+ {renderForm} +
+
+
+ ); +}; diff --git a/src/js/routes/settings.test.tsx b/src/routes/Settings.test.tsx similarity index 81% rename from src/js/routes/settings.test.tsx rename to src/routes/Settings.test.tsx index 38eccfb3c..550397927 100644 --- a/src/js/routes/settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -5,15 +5,10 @@ import { MemoryRouter } from 'react-router-dom'; const { ipcRenderer } = require('electron'); -import { SettingsRoute, mapStateToProps } from './settings'; -import { - SettingsState, - AppState, - AuthState, - EnterpriseAccount, -} from '../../types/reducers'; - -describe('routes/settings.tsx', () => { +import { SettingsRoute } from './Settings'; +import { SettingsState } from '../types'; + +describe('routes/Settings.tsx', () => { const props = { updateSetting: jest.fn(), fetchNotifications: jest.fn(), @@ -42,36 +37,6 @@ describe('routes/settings.tsx', () => { props.history.replace.mockReset(); }); - describe('mapStateToProps', () => { - const state = { - auth: { - token: '123-456', - enterpriseAccounts: [{} as EnterpriseAccount], - } as AuthState, - settings: { - participating: false, - } as SettingsState, - } as AppState; - it('should test the method', () => { - const mappedProps = mapStateToProps(state); - - expect(mappedProps.hasMultipleAccounts).toBeTruthy(); - expect(mappedProps.settings.participating).toBeFalsy(); - }); - - it('should recognize when only one account logged in', () => { - const mappedProps = mapStateToProps({ - ...state, - auth: { - ...state.auth, - token: null, - }, - }); - - expect(mappedProps.hasMultipleAccounts).toBeFalsy(); - }); - }); - it('should render itself & its children', () => { const tree = TestRenderer.create( diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx new file mode 100644 index 000000000..e3222690b --- /dev/null +++ b/src/routes/Settings.tsx @@ -0,0 +1,145 @@ +const { ipcRenderer, remote } = require('electron'); + +import React, { useCallback, useContext, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ArrowLeftIcon } from '@primer/octicons-react'; + +import { AppContext } from '../context/App'; +import { NotificationsContext } from '../context/Notifications'; +import { IconAddAccount } from '../icons/AddAccount'; +import { IconLogOut } from '../icons/Logout'; +import { IconQuit } from '../icons/Quit'; +import { FieldRadioGroup } from '../js/components/fields/radiogroup'; +import { FieldCheckbox } from '../js/components/ui/checkbox'; +import { updateTrayIcon } from '../js/utils/comms'; +import { Appearance } from '../types'; + +const isLinux = remote.process.platform === 'linux'; + +export const SettingsRoute: React.FC = () => { + const { settings, updateSetting, logout } = useContext(AppContext); + const { fetchNotifications } = useContext(NotificationsContext); + const history = useHistory(); + + useEffect(() => { + fetchNotifications(); + }, [settings.participating]); + + const logoutUser = useCallback(() => { + logout(); + history.goBack(); + updateTrayIcon(); + }, []); + + const quitApp = useCallback(() => { + ipcRenderer.send('app-quit'); + }, []); + + const goToEnterprise = useCallback(() => { + return history.replace('/enterpriselogin'); + }, []); + + const footerButtonClass = + 'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none'; + + return ( +
+
+ + +

Settings

+
+ +
+ { + updateSetting('appearance', evt.target.value); + }} + /> + + updateSetting('participating', evt.target.checked)} + /> + updateSetting('playSound', evt.target.checked)} + /> + + updateSetting('showNotifications', evt.target.checked) + } + /> + updateSetting('markOnClick', evt.target.checked)} + /> + {!isLinux && ( + + updateSetting('openAtStartup', evt.target.checked) + } + /> + )} +
+ +
+ + Gitify v{remote.app.getVersion()} + + +
+ + + + + +
+
+
+ ); +}; diff --git a/src/js/routes/__snapshots__/login.test.tsx.snap b/src/routes/__snapshots__/Login.test.tsx.snap similarity index 100% rename from src/js/routes/__snapshots__/login.test.tsx.snap rename to src/routes/__snapshots__/Login.test.tsx.snap diff --git a/src/js/routes/__snapshots__/enterprise-login.test.tsx.snap b/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap similarity index 100% rename from src/js/routes/__snapshots__/enterprise-login.test.tsx.snap rename to src/routes/__snapshots__/LoginEnterprise.test.tsx.snap diff --git a/src/js/routes/__snapshots__/settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap similarity index 100% rename from src/js/routes/__snapshots__/settings.test.tsx.snap rename to src/routes/__snapshots__/Settings.test.tsx.snap diff --git a/src/types.ts b/src/types.ts index 1c781e3a5..5a13d68b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,17 @@ +export interface AuthState { + token?: string; + enterpriseAccounts: EnterpriseAccount[]; +} + +export interface SettingsState { + participating: boolean; + playSound: boolean; + showNotifications: boolean; + markOnClick: boolean; + openAtStartup: boolean; + appearance: Appearance; +} + export enum Appearance { SYSTEM = 'SYSTEM', LIGHT = 'LIGHT', @@ -8,3 +22,22 @@ export type RadioGroupItem = { label: string; value: string; }; + +export interface EnterpriseAccount { + hostname: string; + token: string; +} + +export interface AccountNotifications { + hostname: string; + notifications: Notification[]; +} + +export interface AuthResponse { + hostname: string; + code: string; +} +export interface AuthTokenResponse { + hostname: string; + token: string; +} diff --git a/src/types/actions.ts b/src/types/actions.ts deleted file mode 100644 index 2ee089974..000000000 --- a/src/types/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const LOGOUT = 'LOGOUT'; - -export interface LogoutAction { - type: typeof LOGOUT; -} diff --git a/src/types/reducers.ts b/src/types/reducers.ts deleted file mode 100644 index cedadbe81..000000000 --- a/src/types/reducers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Appearance } from '../types'; -import { Notification } from './github'; - -export interface EnterpriseAccount { - hostname: string; - token: string; -} - -export interface AppState { - auth: AuthState; - notifications: NotificationsState; - settings: SettingsState; -} - -export interface AuthState { - response: {}; - token?: string; - isFetching: boolean; - failed: boolean; - enterpriseAccounts: EnterpriseAccount[]; -} - -export interface AccountNotifications { - hostname: string; - notifications: Notification[]; -} - -export interface NotificationsState { - response: AccountNotifications[]; - isFetching: boolean; - failed: boolean; -} - -export interface SettingsState { - participating: boolean; - playSound: boolean; - showNotifications: boolean; - markOnClick: boolean; - openAtStartup: boolean; - appearance: Appearance; -} diff --git a/src/js/utils/constants.tsx b/src/utils/Constants.ts similarity index 93% rename from src/js/utils/constants.tsx rename to src/utils/Constants.ts index ec8bec046..c97d83b8d 100644 --- a/src/js/utils/constants.tsx +++ b/src/utils/Constants.ts @@ -1,4 +1,4 @@ -export default { +export const Constants = { // GitHub OAuth AUTH_SCOPE: ['user:email', 'notifications'], @@ -42,3 +42,5 @@ export default { ':cry:', ], }; + +export default Constants; diff --git a/tsconfig.json b/tsconfig.json index 7bf1647a5..d565309cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,15 @@ "noImplicitAny": false, "jsx": "react", "allowJs": true, + "allowSyntheticDefaultImports": true, "typeRoots": ["./src/types", "node_modules/@types"] }, "exclude": ["node_modules"], - "include": ["src/**/*.js", "src/**/*.ts", "src/**/*.tsx", "first-run.js", "main.js"] + "include": [ + "src/**/*.js", + "src/**/*.ts", + "src/**/*.tsx", + "first-run.js", + "main.js" + ] } diff --git a/yarn.lock b/yarn.lock index 18a7d5824..65fb26597 100644 --- a/yarn.lock +++ b/yarn.lock @@ -258,6 +258,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1", "@babel/template@^7.10.3", "@babel/template@^7.3.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" @@ -683,7 +690,12 @@ dependencies: "@types/node" "*" -"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": +"@types/history@*": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/hoist-non-react-statics@*": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -778,15 +790,22 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.7": - version "7.1.9" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3" - integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w== +"@types/react-router-dom@^5.1.6": + version "5.1.6" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb" + integrity sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g== dependencies: - "@types/hoist-non-react-statics" "^3.3.0" + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.8" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa" + integrity sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg== + dependencies: + "@types/history" "*" "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" "@types/react-transition-group@^4.2.4": version "4.4.0" @@ -2847,7 +2866,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -3848,26 +3867,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.get@^4.1.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isfunction@^3.0.7, lodash.isfunction@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -3878,21 +3882,6 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.3.1: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.reduce@^4.3.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" - integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= - -lodash.set@^4.0.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -3903,11 +3892,6 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= -lodash.unset@^4.1.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed" - integrity sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0= - lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -4751,7 +4735,7 @@ react-final-form@^6.4.0: "@scarf/scarf" "^1.0.5" ts-essentials "^6.0.5" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -4761,18 +4745,18 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-redux@=7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" - integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== +react-redux@=7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736" + integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA== dependencies: - "@babel/runtime" "^7.5.5" - hoist-non-react-statics "^3.3.0" + "@babel/runtime" "^7.12.1" + hoist-non-react-statics "^3.3.2" loose-envify "^1.4.0" prop-types "^15.7.2" - react-is "^16.9.0" + react-is "^16.13.1" -react-router-dom@^5.1.2: +react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== @@ -4900,18 +4884,6 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -reduce-reducers@^0.1.0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.5.tgz#ff77ca8068ff41007319b8b4b91533c7e0e54576" - integrity sha512-uoVmQnZQ+BtKKDKpBdbBri5SLNyIK9ULZGOA504++VbHcwouWE+fJDIo8AuESPF9/EYSkI0v05LDEQK6stCbTA== - -redux-actions@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-0.10.1.tgz#bb442ee37dd9643a94933e4071e089f435587135" - integrity sha1-u0Qu433ZZDqUkz5AceCJ9DVYcTU= - dependencies: - reduce-reducers "^0.1.0" - redux-mock-store@=1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" @@ -4919,48 +4891,12 @@ redux-mock-store@=1.5.4: dependencies: lodash.isplainobject "^4.0.6" -redux-storage-decorator-filter@=1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/redux-storage-decorator-filter/-/redux-storage-decorator-filter-1.1.8.tgz#c0b7b5563b8ba138ce79c03ad54a2878a3a53ee5" - integrity sha1-wLe1VjuLoTjOecA61UooeKOlPuU= - dependencies: - lodash.get "^4.1.2" - lodash.isfunction "^3.0.7" - lodash.isobject "^3.0.2" - lodash.reduce "^4.3.0" - lodash.set "^4.0.0" - lodash.unset "^4.1.0" - -redux-storage-engine-localstorage@=1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/redux-storage-engine-localstorage/-/redux-storage-engine-localstorage-1.1.4.tgz#2849278d78970f0c3f5f3d4727caa3c30783ed49" - integrity sha1-KEknjXiXDww/Xz1HJ8qjwweD7Uk= - -redux-storage-merger-simple@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/redux-storage-merger-simple/-/redux-storage-merger-simple-1.0.5.tgz#29a2886b0e770d9b70811aca800aa8efae89fb73" - integrity sha1-KaKIaw53DZtwgRrKgAqo766J+3M= - dependencies: - lodash.isobject "^3.0.2" - lodash.merge "^4.3.1" - -redux-storage@=4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux-storage/-/redux-storage-4.1.2.tgz#e06f4bdeee262aead9132fc9f7eadc67e9f9bea2" - integrity sha1-4G9L3u4mKurZEy/J9+rcZ+n5vqI= - dependencies: - lodash.isfunction "^3.0.8" - lodash.isobject "^3.0.2" - loose-envify "^1.2.0" - redux-actions "^0.10.1" - redux-storage-merger-simple "^1.0.2" - redux-thunk@=2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@=4.0.5, redux@^4.0.0: +redux@=4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== From 54b47d8c7d51ca4c8493b9dc7d831120090a15c5 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Tue, 15 Dec 2020 22:01:05 +0000 Subject: [PATCH 02/25] chore: Display notifications successfully --- .../AccountNotifications.test.tsx} | 6 +- .../AccountNotifications.tsx} | 6 +- .../Notification.test.tsx} | 17 +---- .../Notification.tsx} | 43 ++++++------ .../Repository.test.tsx} | 4 +- .../Repository.tsx} | 20 +++--- src/components/Sidebar.test.tsx | 1 - src/components/Sidebar.tsx | 2 +- .../AccountNotifications.test.tsx.snap} | 4 +- src/context/Notifications.tsx | 66 ++++++++++++++++++- src/hooks/useGitHubAuth.ts | 4 +- src/js/__mocks__/mockedData.ts | 2 +- src/js/components/all-read.test.tsx | 2 +- src/js/components/all-read.tsx | 2 +- src/js/components/oops.test.tsx | 2 +- src/js/components/oops.tsx | 2 +- src/js/routes/notifications.test.tsx | 2 +- src/js/utils/github-api.test.ts | 2 +- src/js/utils/github-api.ts | 2 +- src/js/utils/notifications.ts | 3 +- src/types.ts | 2 + src/{types/github.ts => typesGithub.ts} | 0 .../api-requests.js => utils/api-requests.ts} | 15 ++++- 23 files changed, 130 insertions(+), 79 deletions(-) rename src/{js/components/account-notifications.test.tsx => components/AccountNotifications.test.tsx} (79%) rename src/{js/components/account-notifications.tsx => components/AccountNotifications.tsx} (89%) rename src/{js/components/notification.test.tsx => components/Notification.test.tsx} (84%) rename src/{js/components/notification.tsx => components/Notification.tsx} (74%) rename src/{js/components/repository.test.tsx => components/Repository.test.tsx} (91%) rename src/{js/components/repository.tsx => components/Repository.tsx} (81%) rename src/{js/components/__snapshots__/account-notifications.test.tsx.snap => components/__snapshots__/AccountNotifications.test.tsx.snap} (86%) rename src/{types/github.ts => typesGithub.ts} (100%) rename src/{js/utils/api-requests.js => utils/api-requests.ts} (68%) diff --git a/src/js/components/account-notifications.test.tsx b/src/components/AccountNotifications.test.tsx similarity index 79% rename from src/js/components/account-notifications.test.tsx rename to src/components/AccountNotifications.test.tsx index 39c40ed79..67e54adc7 100644 --- a/src/js/components/account-notifications.test.tsx +++ b/src/components/AccountNotifications.test.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import * as TestRendener from 'react-test-renderer'; -import { AccountNotifications } from './account-notifications'; -import { mockedGithubNotifications } from '../__mocks__/mockedData'; +import { AccountNotifications } from './AccountNotifications'; +import { mockedGithubNotifications } from '../js/__mocks__/mockedData'; jest.mock('./repository'); -describe('components/account-notifications.tsx', () => { +describe('components/AccountNotifications.tsx', () => { it('should render itself (github.com with notifications)', () => { const props = { hostname: 'github.com', diff --git a/src/js/components/account-notifications.tsx b/src/components/AccountNotifications.tsx similarity index 89% rename from src/js/components/account-notifications.tsx rename to src/components/AccountNotifications.tsx index bcf66346d..c883d2759 100644 --- a/src/js/components/account-notifications.tsx +++ b/src/components/AccountNotifications.tsx @@ -1,9 +1,9 @@ -import * as React from 'react'; +import React from 'react'; import * as _ from 'lodash'; import { ChevronDownIcon, ChevronLeftIcon } from '@primer/octicons-react'; -import { Notification } from '../../types/github'; -import RepositoryNotifications from './repository'; +import { Notification } from '../typesGithub'; +import { RepositoryNotifications } from './Repository'; interface IProps { hostname: string; diff --git a/src/js/components/notification.test.tsx b/src/components/Notification.test.tsx similarity index 84% rename from src/js/components/notification.test.tsx rename to src/components/Notification.test.tsx index 164945823..fabd795ff 100644 --- a/src/js/components/notification.test.tsx +++ b/src/components/Notification.test.tsx @@ -4,9 +4,8 @@ import { fireEvent, render } from '@testing-library/react'; const { shell } = require('electron'); -import { mockedSingleNotification } from '../__mocks__/mockedData'; -import { NotificationItem, mapStateToProps } from './notification'; -import { SettingsState, AppState } from '../../types/reducers'; +import { mockedSingleNotification } from '../js/__mocks__/mockedData'; +import { NotificationItem } from './notification'; describe('components/notification.js', () => { const notification = mockedSingleNotification; @@ -15,18 +14,6 @@ describe('components/notification.js', () => { spyOn(shell, 'openExternal'); }); - it('should test the mapStateToProps method', () => { - const state = { - settings: { - markOnClick: true, - } as SettingsState, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.markOnClick).toBeTruthy(); - }); - it('should render itself & its children', async () => { (global as any).Date.now = jest.fn(() => new Date('2014')); diff --git a/src/js/components/notification.tsx b/src/components/Notification.tsx similarity index 74% rename from src/js/components/notification.tsx rename to src/components/Notification.tsx index 04e63c1ee..40f188aca 100644 --- a/src/js/components/notification.tsx +++ b/src/components/Notification.tsx @@ -1,29 +1,35 @@ const { shell } = require('electron'); -import * as React from 'react'; -import { connect } from 'react-redux'; +import React, { useCallback, useContext } from 'react'; import { formatDistanceToNow, parseISO } from 'date-fns'; import { CheckIcon, MuteIcon } from '@primer/octicons-react'; -import { AppState } from '../../types/reducers'; -import { formatReason, getNotificationTypeIcon } from '../utils/github-api'; -import { generateGitHubWebUrl } from '../utils/helpers'; -import { markNotification, unsubscribeNotification } from '../actions'; -import { Notification } from '../../types/github'; +import { formatReason, getNotificationTypeIcon } from '../js/utils/github-api'; +import { generateGitHubWebUrl } from '../js/utils/helpers'; +import { Notification } from '../typesGithub'; +import { AppContext } from '../context/App'; interface IProps { hostname: string; notification: Notification; - markOnClick: boolean; - markNotification: (id: string, hostname: string) => void; - unsubscribeNotification?: (id: string, hostname: string) => void; } export const NotificationItem: React.FC = (props) => { + const { settings } = useContext(AppContext); + + const markNotification = useCallback( + async (id: string, hostname: string) => {}, + [] + ); + const unsubscribeNotification = useCallback( + async (id: string, hostname: string) => {}, + [] + ); + const pressTitle = () => { openBrowser(); - if (props.markOnClick) { + if (settings.markOnClick) { markAsRead(); } }; @@ -38,7 +44,7 @@ export const NotificationItem: React.FC = (props) => { const markAsRead = () => { const { hostname, notification } = props; - props.markNotification(notification.id, hostname); + markNotification(notification.id, hostname); }; const unsubscribe = (event: React.MouseEvent) => { @@ -46,7 +52,7 @@ export const NotificationItem: React.FC = (props) => { event.stopPropagation(); const { hostname, notification } = props; - props.unsubscribeNotification(notification.id, hostname); + unsubscribeNotification(notification.id, hostname); }; const { notification } = props; @@ -104,14 +110,3 @@ export const NotificationItem: React.FC = (props) => { ); }; - -export function mapStateToProps(state: AppState) { - return { - markOnClick: state.settings.markOnClick, - }; -} - -export default connect(mapStateToProps, { - markNotification, - unsubscribeNotification, -})(NotificationItem); diff --git a/src/js/components/repository.test.tsx b/src/components/Repository.test.tsx similarity index 91% rename from src/js/components/repository.test.tsx rename to src/components/Repository.test.tsx index ebbfb5bfb..f8bfe0d53 100644 --- a/src/js/components/repository.test.tsx +++ b/src/components/Repository.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; import { render, fireEvent } from '@testing-library/react'; -import { mockedGithubNotifications } from '../__mocks__/mockedData'; -import { RepositoryNotifications } from './repository'; +import { mockedGithubNotifications } from '../js/__mocks__/mockedData'; +import { RepositoryNotifications } from './Repository'; const { shell } = require('electron'); diff --git a/src/js/components/repository.tsx b/src/components/Repository.tsx similarity index 81% rename from src/js/components/repository.tsx rename to src/components/Repository.tsx index 9d06b733c..ff5720d67 100644 --- a/src/js/components/repository.tsx +++ b/src/components/Repository.tsx @@ -1,22 +1,24 @@ const { shell } = require('electron'); -import * as React from 'react'; -import { connect } from 'react-redux'; +import React, { useCallback } from 'react'; import { CheckIcon } from '@primer/octicons-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; -import { markRepoNotifications } from '../actions'; -import { Notification } from '../../types/github'; -import NotificationItem from './notification'; +import { Notification } from '../typesGithub'; +import { NotificationItem } from './notification'; interface IProps { hostname: string; repoNotifications: Notification[]; repoName: string; - markRepoNotifications: (repoSlug: string, hostname: string) => void; } export const RepositoryNotifications: React.FC = (props) => { + const markRepoNotifications = useCallback( + async (repoSlug: string, hostname: string) => {}, + [] + ); + const openBrowser = () => { const url = props.repoNotifications[0].repository.html_url; shell.openExternal(url); @@ -25,7 +27,7 @@ export const RepositoryNotifications: React.FC = (props) => { const markRepoAsRead = () => { const { hostname, repoNotifications } = props; const repoSlug = repoNotifications[0].repository.full_name; - props.markRepoNotifications(repoSlug, hostname); + markRepoNotifications(repoSlug, hostname); }; const { hostname, repoNotifications } = props; @@ -64,7 +66,3 @@ export const RepositoryNotifications: React.FC = (props) => { ); }; - -export default connect(null, { markRepoNotifications })( - RepositoryNotifications -); diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index f53a4ea82..eaf2eb317 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -6,7 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; const { shell, ipcRenderer } = require('electron'); import { AuthState } from '../types'; -import { mapStateToProps } from '../js/components/loading'; import { mockedEnterpriseAccounts } from '../js/__mocks__/mockedData'; import { Sidebar } from './sidebar'; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6ce318fbe..69e824c60 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -5,7 +5,7 @@ import * as Octicons from '@primer/octicons-react'; import { useHistory } from 'react-router-dom'; import { AppContext } from '../context/App'; -import { Constants } from '../utils/Constants'; +import { Constants } from '../utils/constants'; import { Logo } from '../js/components/ui/logo'; import { NotificationsContext } from '../context/Notifications'; diff --git a/src/js/components/__snapshots__/account-notifications.test.tsx.snap b/src/components/__snapshots__/AccountNotifications.test.tsx.snap similarity index 86% rename from src/js/components/__snapshots__/account-notifications.test.tsx.snap rename to src/components/__snapshots__/AccountNotifications.test.tsx.snap index 4656c73a5..eb75618a1 100644 --- a/src/js/components/__snapshots__/account-notifications.test.tsx.snap +++ b/src/components/__snapshots__/AccountNotifications.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/account-notifications.tsx should render itself (github.com with notifications) 1`] = ` +exports[`components/AccountNotifications.tsx should render itself (github.com with notifications) 1`] = `
@@ -29,7 +29,7 @@ exports[`components/account-notifications.tsx should render itself (github.com w
`; -exports[`components/account-notifications.tsx should render itself (github.com without notifications) 1`] = ` +exports[`components/AccountNotifications.tsx should render itself (github.com without notifications) 1`] = `
diff --git a/src/context/Notifications.tsx b/src/context/Notifications.tsx index 235f568d3..b3751c3b1 100644 --- a/src/context/Notifications.tsx +++ b/src/context/Notifications.tsx @@ -1,11 +1,17 @@ -import React, { useState, createContext, useCallback } from 'react'; +import React, { useState, createContext, useCallback, useContext } from 'react'; +import axios from 'axios'; +import { parse } from 'url'; import { AccountNotifications } from '../types'; +import { apiRequestAuth } from '../utils/api-requests'; +import { AppContext } from './App'; +import Constants from '../utils/constants'; interface NotificationsContextState { notifications: AccountNotifications[]; fetchNotifications: () => Promise; isFetching: boolean; + requestFailed: boolean; } export const NotificationsContext = createContext< @@ -17,19 +23,73 @@ export const NotificationsProvider = ({ }: { children: React.ReactNode; }) => { + const { accounts, settings } = useContext(AppContext); const [isFetching, setIsFetching] = useState(false); + const [requestFailed, setRequestFailed] = useState(false); const [notifications, setNotifications] = useState( [] ); const fetchNotifications = useCallback(async () => { - // - }, []); + const isGitHubLoggedIn = accounts.token !== null; + const endpointSuffix = `notifications?participating=${settings.participating}`; + + function getGitHubNotifications() { + if (!isGitHubLoggedIn) { + return; + } + const url = `https://api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}/${endpointSuffix}`; + return apiRequestAuth(url, 'GET', accounts.token); + } + + function getEnterpriseNotifications() { + return accounts.enterpriseAccounts.map((account) => { + const hostname = account.hostname; + const token = account.token; + const url = `https://${hostname}/api/v3/${endpointSuffix}`; + return apiRequestAuth(url, 'GET', token); + }); + } + + setIsFetching(true); + + return axios + .all([getGitHubNotifications(), ...getEnterpriseNotifications()]) + .then( + axios.spread((gitHubNotifications, ...entAccNotifications) => { + const enterpriseNotifications = entAccNotifications.map( + (accountNotifications) => { + const { hostname } = parse(accountNotifications.config.url); + return { + hostname, + notifications: accountNotifications.data, + }; + } + ); + const data = isGitHubLoggedIn + ? [ + ...enterpriseNotifications, + { + hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname, + notifications: gitHubNotifications.data, + }, + ] + : [...enterpriseNotifications]; + + setNotifications(data); + setIsFetching(false); + }) + ) + .catch((error) => { + setIsFetching(false); + }); + }, [accounts]); return ( { const { hostname } = authOptions; diff --git a/src/js/__mocks__/mockedData.ts b/src/js/__mocks__/mockedData.ts index f0c98522b..8f4bccec2 100644 --- a/src/js/__mocks__/mockedData.ts +++ b/src/js/__mocks__/mockedData.ts @@ -1,5 +1,5 @@ import { EnterpriseAccount } from '../../types'; -import { Notification, Repository } from '../../types/github'; +import { Notification, Repository } from '../../typesGithub'; export const mockedEnterpriseAccounts: EnterpriseAccount[] = [ { diff --git a/src/js/components/all-read.test.tsx b/src/js/components/all-read.test.tsx index 37d5997dd..22fc26bf9 100644 --- a/src/js/components/all-read.test.tsx +++ b/src/js/components/all-read.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; -import { Constants } from '../../utils/Constants'; +import { Constants } from '../../utils/constants'; import { AllRead } from './all-read'; diff --git a/src/js/components/all-read.tsx b/src/js/components/all-read.tsx index 45e59fc9f..b32006aca 100644 --- a/src/js/components/all-read.tsx +++ b/src/js/components/all-read.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Typist from 'react-typist'; import { emojify } from 'react-emojione'; -import { Constants } from '../../utils/Constants'; +import { Constants } from '../../utils/constants'; export const AllRead = () => { const message = React.useMemo( diff --git a/src/js/components/oops.test.tsx b/src/js/components/oops.test.tsx index 57d36bc26..f12dadd40 100644 --- a/src/js/components/oops.test.tsx +++ b/src/js/components/oops.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; -import { Constants } from '../../utils/Constants'; +import { Constants } from '../../utils/constants'; import { Oops } from './oops'; diff --git a/src/js/components/oops.tsx b/src/js/components/oops.tsx index ceb4982be..b5212717a 100644 --- a/src/js/components/oops.tsx +++ b/src/js/components/oops.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { emojify } from 'react-emojione'; -import { Constants } from '../../utils/Constants'; +import { Constants } from '../../utils/constants'; export const Oops = () => { const emoji = React.useMemo( diff --git a/src/js/routes/notifications.test.tsx b/src/js/routes/notifications.test.tsx index 2f5464990..ee36bb099 100644 --- a/src/js/routes/notifications.test.tsx +++ b/src/js/routes/notifications.test.tsx @@ -5,7 +5,7 @@ import { AppState, NotificationsState } from '../../types/reducers'; import { mockedNotificationsReducerData } from '../__mocks__/mockedData'; import { NotificationsRoute, mapStateToProps } from './notifications'; -jest.mock('../components/account-notifications', () => ({ +jest.mock('../../components/AccountNotifications', () => ({ AccountNotifications: 'AccountNotifications', })); diff --git a/src/js/utils/github-api.test.ts b/src/js/utils/github-api.test.ts index e0b180f00..b146c99fc 100644 --- a/src/js/utils/github-api.test.ts +++ b/src/js/utils/github-api.test.ts @@ -1,5 +1,5 @@ import { formatReason, getNotificationTypeIcon } from './github-api'; -import { Reason, SubjectType } from '../../types/github'; +import { Reason, SubjectType } from '../../typesGithub'; describe('./utils/github-api.ts', () => { it('should format the notification reason', () => { diff --git a/src/js/utils/github-api.ts b/src/js/utils/github-api.ts index 521ebf465..ec36741ca 100644 --- a/src/js/utils/github-api.ts +++ b/src/js/utils/github-api.ts @@ -1,4 +1,4 @@ -import { Reason, SubjectType } from '../../types/github'; +import { Reason, SubjectType } from '../../typesGithub'; import * as Octicons from '@primer/octicons-react'; // prettier-ignore diff --git a/src/js/utils/notifications.ts b/src/js/utils/notifications.ts index 36878f8d0..37a6c3b0b 100644 --- a/src/js/utils/notifications.ts +++ b/src/js/utils/notifications.ts @@ -2,7 +2,8 @@ const { remote } = require('electron'); import { generateGitHubWebUrl } from '../utils/helpers'; import { reOpenWindow, openExternalLink } from '../utils/comms'; -import { Notification } from '../../types/github'; +import { Notification } from '../../typesGithub'; + import { SettingsState } from '../../types'; export default { diff --git a/src/types.ts b/src/types.ts index 5a13d68b2..638e2d463 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { Notification } from './typesGithub'; + export interface AuthState { token?: string; enterpriseAccounts: EnterpriseAccount[]; diff --git a/src/types/github.ts b/src/typesGithub.ts similarity index 100% rename from src/types/github.ts rename to src/typesGithub.ts diff --git a/src/js/utils/api-requests.js b/src/utils/api-requests.ts similarity index 68% rename from src/js/utils/api-requests.js rename to src/utils/api-requests.ts index d5f0e32c7..8851d8462 100644 --- a/src/js/utils/api-requests.js +++ b/src/utils/api-requests.ts @@ -1,13 +1,22 @@ -import axios from 'axios'; +import axios, { AxiosPromise, Method } from 'axios'; -export function apiRequest(url, method, data = {}) { +export function apiRequest( + url: string, + method: Method, + data = {} +): AxiosPromise { axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['Content-Type'] = 'application/json'; axios.defaults.headers.common['Cache-Control'] = 'no-cache'; return axios({ method, url, data }); } -export function apiRequestAuth(url, method, token, data = {}) { +export function apiRequestAuth( + url: string, + method: Method, + token: string, + data = {} +): AxiosPromise { axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['Authorization'] = `token ${token}`; axios.defaults.headers.common['Cache-Control'] = 'no-cache'; From 289c41ce5bd9e6f3a48307b6a0edf8707205a0a1 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Tue, 15 Dec 2020 22:37:24 +0000 Subject: [PATCH 03/25] chore: Fix a few tests --- src/components/AccountNotifications.tsx | 2 +- src/components/Loading.test.tsx | 31 +++++-- src/components/Loading.tsx | 8 +- .../reducers/__snapshots__/auth.test.ts.snap | 67 -------------- src/js/reducers/auth.test.ts | 90 ------------------- src/js/reducers/auth.ts | 57 ------------ tsconfig.json | 1 + 7 files changed, 28 insertions(+), 228 deletions(-) delete mode 100644 src/js/reducers/__snapshots__/auth.test.ts.snap delete mode 100644 src/js/reducers/auth.test.ts delete mode 100644 src/js/reducers/auth.ts diff --git a/src/components/AccountNotifications.tsx b/src/components/AccountNotifications.tsx index c883d2759..bd2e9e82f 100644 --- a/src/components/AccountNotifications.tsx +++ b/src/components/AccountNotifications.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import * as _ from 'lodash'; +import _ from 'lodash'; import { ChevronDownIcon, ChevronLeftIcon } from '@primer/octicons-react'; import { Notification } from '../typesGithub'; diff --git a/src/components/Loading.test.tsx b/src/components/Loading.test.tsx index 8290bf9ee..9c52c0aa7 100644 --- a/src/components/Loading.test.tsx +++ b/src/components/Loading.test.tsx @@ -1,8 +1,9 @@ -import * as React from 'react'; +import React from 'react'; import { render } from '@testing-library/react'; -import * as NProgress from 'nprogress'; +import NProgress from 'nprogress'; import { Loading } from './loading'; +import { NotificationsContext } from '../context/Notifications'; jest.mock('nprogress', () => { return { @@ -13,7 +14,7 @@ jest.mock('nprogress', () => { }; }); -describe('components/loading.js', function () { +describe('components/Loading.js', () => { beforeEach(() => { NProgress.configure.mockReset(); NProgress.start.mockReset(); @@ -21,24 +22,36 @@ describe('components/loading.js', function () { NProgress.remove.mockReset(); }); - it('should check that NProgress is getting called in getDerivedStateFromProps (loading)', function () { - const { container } = render(); + it('should check that NProgress is getting called in when isFetching changes (loading)', () => { + const { container } = render( + + + + ); expect(container.innerHTML).toBe(''); expect(NProgress.configure).toHaveBeenCalledTimes(1); expect(NProgress.start).toHaveBeenCalledTimes(1); }); - it('should check that NProgress is getting called in getDerivedStateFromProps (not loading)', function () { - const { container } = render(); + it('should check that NProgress is getting called in when isFetching changes (not loading)', () => { + const { container } = render( + + + + ); expect(container.innerHTML).toBe(''); expect(NProgress.configure).toHaveBeenCalledTimes(1); expect(NProgress.done).toHaveBeenCalledTimes(1); }); - it('should remove NProgress on unmount', function () { - const { unmount } = render(); + it('should remove NProgress on unmount', () => { + const { unmount } = render( + + + + ); expect(NProgress.remove).toHaveBeenCalledTimes(0); unmount(); expect(NProgress.remove).toHaveBeenCalledTimes(1); diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index 22887abc4..10fffe1a2 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -1,11 +1,11 @@ -import React, { useContext } from 'react'; -import * as NProgress from 'nprogress'; +import React, { useContext, useEffect } from 'react'; +import NProgress from 'nprogress'; import { NotificationsContext } from '../context/Notifications'; export const Loading = () => { const { isFetching } = useContext(NotificationsContext); - React.useEffect(() => { + useEffect(() => { NProgress.configure({ showSpinner: false, }); @@ -15,7 +15,7 @@ export const Loading = () => { }; }, []); - React.useEffect(() => { + useEffect(() => { if (isFetching) { NProgress.start(); } else { diff --git a/src/js/reducers/__snapshots__/auth.test.ts.snap b/src/js/reducers/__snapshots__/auth.test.ts.snap deleted file mode 100644 index a7e1374b4..000000000 --- a/src/js/reducers/__snapshots__/auth.test.ts.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducers/auth.ts should handle LOGIN.FAILURE 1`] = ` -Object { - "failed": true, - "isFetching": false, - "response": Object { - "msg": "Failed to login.", - }, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.REQUEST 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": true, - "response": Object {}, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.SUCCESS - enterprise 1`] = ` -Object { - "enterpriseAccounts": Array [ - Object { - "hostname": "github.gitify.io", - "token": "123HELLOWORLDTOKEN", - }, - ], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.SUCCESS - github.com 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": "123HELLOWORLDTOKEN", -} -`; - -exports[`reducers/auth.ts should handle LOGOUT 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": null, - "token": null, -} -`; - -exports[`reducers/auth.ts should return the initial state 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": null, -} -`; diff --git a/src/js/reducers/auth.test.ts b/src/js/reducers/auth.test.ts deleted file mode 100644 index e9f36e5ae..000000000 --- a/src/js/reducers/auth.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { AuthState } from '../../types/reducers'; -import { LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import reducer from './auth'; - -describe('reducers/auth.ts', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - }); - - it('should handle LOGIN.REQUEST', () => { - const action = { - type: LOGIN.REQUEST, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle LOGIN.SUCCESS - github.com', () => { - const fakeState = { - response: {}, - token: null, - isFetching: true, - failed: false, - } as AuthState; - - expect(reducer(fakeState, {}).token).toBeNull(); - - const action = { - type: LOGIN.SUCCESS, - payload: { - access_token: '123HELLOWORLDTOKEN', - }, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBe('123HELLOWORLDTOKEN'); - }); - - it('should handle LOGIN.SUCCESS - enterprise', () => { - const action = { - type: LOGIN.SUCCESS, - isEnterprise: true, - hostname: 'github.gitify.io', - payload: { - access_token: '123HELLOWORLDTOKEN', - }, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle LOGIN.FAILURE', () => { - const fakeState = { - response: {}, - token: null, - isFetching: true, - failed: false, - } as AuthState; - - expect(reducer(fakeState, {}).token).toBeNull(); - - const action = { - type: LOGIN.FAILURE, - payload: { msg: 'Failed to login.' }, - }; - - expect(reducer(fakeState, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBeNull(); - }); - - it('should handle LOGOUT', () => { - const fakeState = { - response: {}, - token: 'LOGGEDINTOKEN', - isFetching: false, - failed: false, - enterpriseAccounts: [], - } as AuthState; - - expect(reducer(fakeState, {}).token).not.toBeNull(); - - const action = { - type: LOGOUT, - }; - - expect(reducer(fakeState, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBeNull(); - }); -}); diff --git a/src/js/reducers/auth.ts b/src/js/reducers/auth.ts deleted file mode 100644 index 96a94edb0..000000000 --- a/src/js/reducers/auth.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import { AuthState, EnterpriseAccount } from '../../types/reducers'; - -const initialState: AuthState = { - response: {}, - token: null, - isFetching: false, - failed: false, - enterpriseAccounts: [], -}; - -export default function reducer(state = initialState, action): AuthState { - switch (action.type) { - case LOGIN.REQUEST: - return { - ...state, - isFetching: true, - failed: false, - response: {}, - }; - case LOGIN.SUCCESS: - if (action.isEnterprise) { - const enterpriseAccount: EnterpriseAccount = { - hostname: action.hostname, - token: action.payload.access_token, - }; - return { - ...state, - isFetching: false, - enterpriseAccounts: [...state.enterpriseAccounts, enterpriseAccount], - }; - } - - return { - ...state, - isFetching: false, - token: action.payload.access_token, - }; - case LOGIN.FAILURE: - return { - ...state, - isFetching: false, - failed: true, - response: action.payload, - }; - case LOGOUT: - return { - ...state, - response: null, - token: null, - enterpriseAccounts: [], - }; - default: - return state; - } -} diff --git a/tsconfig.json b/tsconfig.json index d565309cd..1061412be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "typeRoots": ["./src/types", "node_modules/@types"] }, "exclude": ["node_modules"], From e5eb6f22ef401cf2744d9b315398f0679acfbe1a Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 16 Dec 2020 00:52:42 +0000 Subject: [PATCH 04/25] chore: Move more files --- .../AllRead.test.tsx} | 4 +- .../all-read.tsx => components/AllRead.tsx} | 2 +- src/components/Notification.tsx | 2 +- .../Oops.test.tsx} | 4 +- .../oops.tsx => components/Oops.tsx} | 2 +- .../__snapshots__/AllRead.test.tsx.snap} | 0 .../__snapshots__/Notification.test.tsx.snap} | 0 .../__snapshots__/Oops.test.tsx.snap} | 0 .../__snapshots__/Repository.test.tsx.snap} | 0 src/js/routes/notifications.tsx | 58 ----- src/js/utils/helpers.test.ts | 206 ------------------ src/js/utils/helpers.ts | 108 --------- src/routes/Login.test.tsx | 2 +- .../Notifications.test.tsx} | 5 +- src/routes/Notifications.tsx | 43 ++++ .../Notifications.test.tsx.snap} | 0 src/utils/helpers.test.ts | 65 ++++++ src/utils/helpers.ts | 39 ++++ 18 files changed, 158 insertions(+), 382 deletions(-) rename src/{js/components/all-read.test.tsx => components/AllRead.test.tsx} (81%) rename src/{js/components/all-read.tsx => components/AllRead.tsx} (94%) rename src/{js/components/oops.test.tsx => components/Oops.test.tsx} (79%) rename src/{js/components/oops.tsx => components/Oops.tsx} (92%) rename src/{js/components/__snapshots__/all-read.test.tsx.snap => components/__snapshots__/AllRead.test.tsx.snap} (100%) rename src/{js/components/__snapshots__/notification.test.tsx.snap => components/__snapshots__/Notification.test.tsx.snap} (100%) rename src/{js/components/__snapshots__/oops.test.tsx.snap => components/__snapshots__/Oops.test.tsx.snap} (100%) rename src/{js/components/__snapshots__/repository.test.tsx.snap => components/__snapshots__/Repository.test.tsx.snap} (100%) delete mode 100644 src/js/routes/notifications.tsx delete mode 100644 src/js/utils/helpers.test.ts delete mode 100644 src/js/utils/helpers.ts rename src/{js/routes/notifications.test.tsx => routes/Notifications.test.tsx} (92%) create mode 100644 src/routes/Notifications.tsx rename src/{js/routes/__snapshots__/notifications.test.tsx.snap => routes/__snapshots__/Notifications.test.tsx.snap} (100%) create mode 100644 src/utils/helpers.test.ts create mode 100644 src/utils/helpers.ts diff --git a/src/js/components/all-read.test.tsx b/src/components/AllRead.test.tsx similarity index 81% rename from src/js/components/all-read.test.tsx rename to src/components/AllRead.test.tsx index 22fc26bf9..5e7115e81 100644 --- a/src/js/components/all-read.test.tsx +++ b/src/components/AllRead.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; -import { Constants } from '../../utils/constants'; +import { Constants } from '../utils/constants'; -import { AllRead } from './all-read'; +import { AllRead } from './AllRead'; jest.mock('react-typist'); diff --git a/src/js/components/all-read.tsx b/src/components/AllRead.tsx similarity index 94% rename from src/js/components/all-read.tsx rename to src/components/AllRead.tsx index b32006aca..b58271508 100644 --- a/src/js/components/all-read.tsx +++ b/src/components/AllRead.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Typist from 'react-typist'; import { emojify } from 'react-emojione'; -import { Constants } from '../../utils/constants'; +import { Constants } from '../utils/constants'; export const AllRead = () => { const message = React.useMemo( diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index 40f188aca..de9133af4 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -5,7 +5,7 @@ import { formatDistanceToNow, parseISO } from 'date-fns'; import { CheckIcon, MuteIcon } from '@primer/octicons-react'; import { formatReason, getNotificationTypeIcon } from '../js/utils/github-api'; -import { generateGitHubWebUrl } from '../js/utils/helpers'; +import { generateGitHubWebUrl } from '../utils/helpers'; import { Notification } from '../typesGithub'; import { AppContext } from '../context/App'; diff --git a/src/js/components/oops.test.tsx b/src/components/Oops.test.tsx similarity index 79% rename from src/js/components/oops.test.tsx rename to src/components/Oops.test.tsx index f12dadd40..d21c10905 100644 --- a/src/js/components/oops.test.tsx +++ b/src/components/Oops.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; -import { Constants } from '../../utils/constants'; +import { Constants } from '../utils/constants'; -import { Oops } from './oops'; +import { Oops } from './Oops'; describe('components/oops.tsx', function () { it('should render itself & its children', function () { diff --git a/src/js/components/oops.tsx b/src/components/Oops.tsx similarity index 92% rename from src/js/components/oops.tsx rename to src/components/Oops.tsx index b5212717a..d41d6efbc 100644 --- a/src/js/components/oops.tsx +++ b/src/components/Oops.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { emojify } from 'react-emojione'; -import { Constants } from '../../utils/constants'; +import { Constants } from '../utils/constants'; export const Oops = () => { const emoji = React.useMemo( diff --git a/src/js/components/__snapshots__/all-read.test.tsx.snap b/src/components/__snapshots__/AllRead.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/all-read.test.tsx.snap rename to src/components/__snapshots__/AllRead.test.tsx.snap diff --git a/src/js/components/__snapshots__/notification.test.tsx.snap b/src/components/__snapshots__/Notification.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/notification.test.tsx.snap rename to src/components/__snapshots__/Notification.test.tsx.snap diff --git a/src/js/components/__snapshots__/oops.test.tsx.snap b/src/components/__snapshots__/Oops.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/oops.test.tsx.snap rename to src/components/__snapshots__/Oops.test.tsx.snap diff --git a/src/js/components/__snapshots__/repository.test.tsx.snap b/src/components/__snapshots__/Repository.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/repository.test.tsx.snap rename to src/components/__snapshots__/Repository.test.tsx.snap diff --git a/src/js/routes/notifications.tsx b/src/js/routes/notifications.tsx deleted file mode 100644 index bba8ab88d..000000000 --- a/src/js/routes/notifications.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; - -import { AccountNotifications as Account } from '../components/account-notifications'; -import { AllRead } from '../components/all-read'; -import { AppState, AccountNotifications } from '../../types/reducers'; -import { Oops } from '../components/oops'; - -interface IProps { - hasMultipleAccounts: boolean; - hasNotifications: boolean; - accountNotifications: AccountNotifications[]; - notificationsCount: number; - failed: boolean; -} - -export const NotificationsRoute = (props: IProps) => { - const { accountNotifications, hasMultipleAccounts, hasNotifications } = props; - - if (props.failed) { - return ; - } - - if (!hasNotifications) { - return ; - } - - return ( -
- {accountNotifications.map((account) => ( - - ))} -
- ); -}; - -export function mapStateToProps(state: AppState) { - const notificationsCount = state.notifications.response.reduce( - (memo, acc) => memo + acc.notifications.length, - 0 - ); - const hasMultipleAccounts = state.notifications.response.length > 1; - - return { - failed: state.notifications.failed, - accountNotifications: state.notifications.response, - notificationsCount, - hasMultipleAccounts, - hasNotifications: notificationsCount > 0, - }; -} - -export default connect(mapStateToProps, null)(NotificationsRoute); diff --git a/src/js/utils/helpers.test.ts b/src/js/utils/helpers.test.ts deleted file mode 100644 index 61a8d4b88..000000000 --- a/src/js/utils/helpers.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -const { remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; -const dialog = remote.dialog; - -import { AuthState, EnterpriseAccount } from '../../types'; -import { - authGithub, - generateGitHubWebUrl, - generateGitHubAPIUrl, - isUserEitherLoggedIn, -} from './helpers'; - -describe('utils/helpers.ts', () => { - it('should generate the GitHub url - non enterprise - (issue)', () => { - const apiUrl = - 'https://api.github.com/repos/ekonstantinidis/notifications-test/issues/3'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.com/ekonstantinidis/notifications-test/issues/3' - ); - }); - - it('should generate the GitHub url - non enterprise - (pull request)', () => { - const apiUrl = - 'https://api.github.com/repos/ekonstantinidis/notifications-test/pulls/123'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.com/ekonstantinidis/notifications-test/pull/123' - ); - }); - - it('should generate the GitHub url - non enterprise - (release)', () => { - const apiUrl = - 'https://api.github.com/repos/myorg/notifications-test/releases/3988077'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe('https://github.com/myorg/notifications-test/releases'); - }); - - it('should generate the GitHub url - enterprise - (issue)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/123'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/issues/123' - ); - }); - - it('should generate the GitHub url - enterprise - (pull request)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/3'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/pull/3' - ); - }); - - it('should generate the GitHub url - enterprise - (release)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/1'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/releases' - ); - }); - - it('should generate a GitHub API url - non enterprise', () => { - const result = generateGitHubAPIUrl('github.com'); - expect(result).toBe('https://api.github.com/'); - }); - - it('should generate a GitHub API url - enterprise', () => { - const result = generateGitHubAPIUrl('github.manos.im'); - expect(result).toBe('https://github.manos.im/api/v3/'); - }); - - it('should test isUserEitherLoggedIn - with github', () => { - const auth = { - token: '123-456', - enterpriseAccounts: [], - } as AuthState; - const result = isUserEitherLoggedIn(auth); - expect(result).toBeTruthy(); - }); - - it('should test isUserEitherLoggedIn - with enterprise', () => { - const auth = { - token: null, - enterpriseAccounts: [{} as EnterpriseAccount], - } as AuthState; - const result = isUserEitherLoggedIn(auth); - expect(result).toBeTruthy(); - }); - - it('should test the authGitHub - success', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'will-redirect') { - const event = new Event('will-redirect'); - callback(event, 'http://github.com/?code=123-456'); - } - } - ); - - authGithub(undefined, dispatch); - - expect( - new BrowserWindow().webContents.session.clearStorageData - ).toHaveBeenCalledTimes(1); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - expect(new BrowserWindow().loadURL).toHaveBeenCalledWith( - 'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=user:email,notifications' - ); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dispatch).toHaveBeenCalledTimes(1); - }); - - it('should test the authGitHub - with error', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'will-redirect') { - const event = new Event('will-redirect'); - callback(event, 'http://www.github.com/?error=Oops'); - } - } - ); - - expect( - new BrowserWindow().webContents.session.clearStorageData - ).toHaveBeenCalledTimes(1); - - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - - expect(window.alert).toHaveBeenCalledTimes(1); - expect(window.alert).toHaveBeenCalledWith( - "Oops! Something went wrong and we couldn't log you in using Github. Please try again." - ); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('should test the authGitHub - fail to load the page', () => { - const dispatch = jest.fn(); - - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - // @ts-ignore - new BrowserWindow().destroy.mockReset(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'did-fail-load') { - const event = new Event('did-fail-load'); - callback(event, 500, null, 'http://github.com/?code=123-456'); - } - } - ); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - expect(new BrowserWindow().loadURL).toHaveBeenCalledWith( - 'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=user:email,notifications' - ); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dialog.showErrorBox).toHaveBeenCalledTimes(1); - expect(dialog.showErrorBox).toHaveBeenCalledWith( - 'Invalid Hostname', - 'Could not load https://github.com/.' - ); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('should destroy the auth window on close', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow(), 'on').and.callFake((event, callback) => { - if (event === 'close') { - callback(); - } - }); - - // @ts-ignore - new BrowserWindow().destroy.mockReset(); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dispatch).not.toHaveBeenCalled(); - }); -}); diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts deleted file mode 100644 index 685649304..000000000 --- a/src/js/utils/helpers.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { parse } from 'url'; -const { remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; -const dialog = remote.dialog; - -import Constants from './constants'; -import { loginUser } from '../actions'; -import { AuthState } from '../../types'; - -export function getEnterpriseAccountToken(hostname, accounts): string { - return accounts.find((obj) => obj.hostname === hostname).token; -} - -export function generateGitHubAPIUrl(hostname) { - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - return isEnterprise - ? `https://${hostname}/api/v3/` - : `https://api.${hostname}/`; -} - -export function generateGitHubWebUrl(url: string) { - const { hostname } = parse(url); - const isEnterprise = - hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`; - - let newUrl: string = isEnterprise - ? url.replace(`${hostname}/api/v3/repos`, hostname) - : url.replace('api.github.com/repos', 'github.com'); - - if (newUrl.indexOf('/pulls/') !== -1) { - newUrl = newUrl.replace('/pulls/', '/pull/'); - } - - if (newUrl.indexOf('/releases/') !== -1) { - newUrl = newUrl.replace('/repos', ''); - newUrl = newUrl.substr(0, newUrl.lastIndexOf('/')); - } - - return newUrl; -} - -export function authGithub( - authOptions = Constants.DEFAULT_AUTH_OPTIONS, - dispatch -) { - // Build the OAuth consent page URL - const authWindow = new BrowserWindow({ - width: 800, - height: 600, - show: true, - }); - - const githubUrl = `https://${authOptions.hostname}/login/oauth/authorize`; - const authUrl = `${githubUrl}?client_id=${authOptions.clientId}&scope=${Constants.AUTH_SCOPE}`; - - const session = authWindow.webContents.session; - session.clearStorageData(); - - authWindow.loadURL(authUrl); - - function handleCallback(url) { - const raw_code = /code=([^&]*)/.exec(url) || null; - const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; - const error = /\?error=(.+)$/.exec(url); - - if (code || error) { - // Close the browser if code found or error - authWindow.destroy(); - } - - // If there is a code, proceed to get token from github - if (code) { - dispatch(loginUser(authOptions, code)); - } else if (error) { - alert( - "Oops! Something went wrong and we couldn't " + - 'log you in using Github. Please try again.' - ); - } - } - - // If "Done" button is pressed, hide "Loading" - authWindow.on('close', () => { - authWindow.destroy(); - }); - - authWindow.webContents.on( - 'did-fail-load', - (event, errorCode, errorDescription, validatedURL) => { - if (validatedURL.includes(authOptions.hostname)) { - authWindow.destroy(); - dialog.showErrorBox( - 'Invalid Hostname', - `Could not load https://${authOptions.hostname}/.` - ); - } - } - ); - - authWindow.webContents.on('will-redirect', (event, url) => { - event.preventDefault(); - handleCallback(url); - }); -} - -export function isUserEitherLoggedIn(auth: AuthState) { - return auth.token !== null || auth.enterpriseAccounts.length > 0; -} diff --git a/src/routes/Login.test.tsx b/src/routes/Login.test.tsx index 12d6bfc87..eb94dbb72 100644 --- a/src/routes/Login.test.tsx +++ b/src/routes/Login.test.tsx @@ -8,7 +8,7 @@ const { ipcRenderer, remote } = require('electron'); const BrowserWindow = remote.BrowserWindow; import { LoginRoute } from './login'; -import * as helpers from '../js/utils/helpers'; +import * as helpers from '../utils/helpers'; describe('routes/login.tsx', () => { const props = { diff --git a/src/js/routes/notifications.test.tsx b/src/routes/Notifications.test.tsx similarity index 92% rename from src/js/routes/notifications.test.tsx rename to src/routes/Notifications.test.tsx index ee36bb099..02a2d2ece 100644 --- a/src/js/routes/notifications.test.tsx +++ b/src/routes/Notifications.test.tsx @@ -1,9 +1,10 @@ +// @ts-nocheck import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; import { AppState, NotificationsState } from '../../types/reducers'; -import { mockedNotificationsReducerData } from '../__mocks__/mockedData'; -import { NotificationsRoute, mapStateToProps } from './notifications'; +import { mockedNotificationsReducerData } from '../js/__mocks__/mockedData'; +import { NotificationsRoute, mapStateToProps } from './Notifications'; jest.mock('../../components/AccountNotifications', () => ({ AccountNotifications: 'AccountNotifications', diff --git a/src/routes/Notifications.tsx b/src/routes/Notifications.tsx new file mode 100644 index 000000000..96ede6d44 --- /dev/null +++ b/src/routes/Notifications.tsx @@ -0,0 +1,43 @@ +import React, { useContext, useMemo } from 'react'; +import { NotificationsContext } from '../context/Notifications'; + +import { AccountNotifications } from '../components/AccountNotifications'; +import { AllRead } from '../components/AllRead'; +import { Oops } from '../components/Oops'; + +export const NotificationsRoute: React.FC = (props) => { + const { notifications, requestFailed } = useContext(NotificationsContext); + + const hasMultipleAccounts = useMemo(() => notifications.length > 1, [ + notifications, + ]); + const notificationsCount = useMemo( + () => + notifications.reduce((memo, acc) => memo + acc.notifications.length, 0), + [notifications] + ); + const hasNotifications = useMemo(() => notificationsCount > 0, [ + notificationsCount, + ]); + + if (requestFailed) { + return ; + } + + if (!hasNotifications) { + return ; + } + + return ( +
+ {notifications.map((account) => ( + + ))} +
+ ); +}; diff --git a/src/js/routes/__snapshots__/notifications.test.tsx.snap b/src/routes/__snapshots__/Notifications.test.tsx.snap similarity index 100% rename from src/js/routes/__snapshots__/notifications.test.tsx.snap rename to src/routes/__snapshots__/Notifications.test.tsx.snap diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts new file mode 100644 index 000000000..05951fed2 --- /dev/null +++ b/src/utils/helpers.test.ts @@ -0,0 +1,65 @@ +import { generateGitHubWebUrl, generateGitHubAPIUrl } from './helpers'; + +describe('utils/helpers.ts', () => { + it('should generate the GitHub url - non enterprise - (issue)', () => { + const apiUrl = + 'https://api.github.com/repos/ekonstantinidis/notifications-test/issues/3'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.com/ekonstantinidis/notifications-test/issues/3' + ); + }); + + it('should generate the GitHub url - non enterprise - (pull request)', () => { + const apiUrl = + 'https://api.github.com/repos/ekonstantinidis/notifications-test/pulls/123'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.com/ekonstantinidis/notifications-test/pull/123' + ); + }); + + it('should generate the GitHub url - non enterprise - (release)', () => { + const apiUrl = + 'https://api.github.com/repos/myorg/notifications-test/releases/3988077'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe('https://github.com/myorg/notifications-test/releases'); + }); + + it('should generate the GitHub url - enterprise - (issue)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/123'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/issues/123' + ); + }); + + it('should generate the GitHub url - enterprise - (pull request)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/3'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/pull/3' + ); + }); + + it('should generate the GitHub url - enterprise - (release)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/1'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/releases' + ); + }); + + it('should generate a GitHub API url - non enterprise', () => { + const result = generateGitHubAPIUrl('github.com'); + expect(result).toBe('https://api.github.com/'); + }); + + it('should generate a GitHub API url - enterprise', () => { + const result = generateGitHubAPIUrl('github.manos.im'); + expect(result).toBe('https://github.manos.im/api/v3/'); + }); +}); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 000000000..515a598e9 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,39 @@ +import { parse } from 'url'; +import { EnterpriseAccount } from '../types'; + +import { Constants } from './constants'; + +export function getEnterpriseAccountToken( + hostname: string, + accounts: EnterpriseAccount[] +): string { + return accounts.find((obj) => obj.hostname === hostname).token; +} + +export function generateGitHubAPIUrl(hostname) { + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + return isEnterprise + ? `https://${hostname}/api/v3/` + : `https://api.${hostname}/`; +} + +export function generateGitHubWebUrl(url: string) { + const { hostname } = parse(url); + const isEnterprise = + hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`; + + let newUrl: string = isEnterprise + ? url.replace(`${hostname}/api/v3/repos`, hostname) + : url.replace('api.github.com/repos', 'github.com'); + + if (newUrl.indexOf('/pulls/') !== -1) { + newUrl = newUrl.replace('/pulls/', '/pull/'); + } + + if (newUrl.indexOf('/releases/') !== -1) { + newUrl = newUrl.replace('/repos', ''); + newUrl = newUrl.substr(0, newUrl.lastIndexOf('/')); + } + + return newUrl; +} From 48a8b538b6ad846923efbe269016fca68e212205 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 16 Dec 2020 01:55:46 +0000 Subject: [PATCH 05/25] chore: Restore Settings on launch --- src/components/Sidebar.tsx | 2 +- src/context/App.tsx | 45 ++++++++++++++++++++------- src/context/Notifications.tsx | 2 +- src/js/app.tsx | 2 +- src/js/middleware/settings.test.ts | 29 ----------------- src/js/middleware/settings.ts | 18 ----------- src/js/utils/comms.ts | 2 +- src/js/utils/notifications.test.ts | 2 +- src/js/utils/notifications.ts | 2 +- src/{js => }/utils/appearance.test.ts | 2 +- src/{js => }/utils/appearance.ts | 2 +- src/utils/storage.ts | 23 ++++++++++++++ 12 files changed, 64 insertions(+), 67 deletions(-) delete mode 100644 src/js/middleware/settings.test.ts delete mode 100644 src/js/middleware/settings.ts rename src/{js => }/utils/appearance.test.ts (96%) rename src/{js => }/utils/appearance.ts (93%) create mode 100644 src/utils/storage.ts diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 69e824c60..51a629393 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -49,7 +49,7 @@ export const Sidebar: React.FC = () => { return history.goBack(); } return history.push('/settings'); - }, []); + }, [location]); const notificationsCount = useMemo(() => { return notifications.reduce( diff --git a/src/context/App.tsx b/src/context/App.tsx index 8a2ec95e7..738dac559 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -5,9 +5,12 @@ import React, { useEffect, useMemo, } from 'react'; -import { useGitHubAuth } from '../hooks/useGitHubAuth'; import { Appearance, AuthState, SettingsState } from '../types'; +import { clearState, loadState, saveState } from '../utils/storage'; +import { setAppearance } from '../utils/appearance'; +import { setAutoLaunch } from '../js/utils/comms'; +import { useGitHubAuth } from '../hooks/useGitHubAuth'; const defaultAccounts: AuthState = { token: null, @@ -40,9 +43,21 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [settings, setSettings] = useState(defaultSettings); const { authGitHub, getToken } = useGitHubAuth(); - const updateSetting = useCallback((name: keyof SettingsState, value: any) => { - setSettings({ ...settings, [name]: value }); - }, []); + const updateSetting = useCallback( + (name: keyof SettingsState, value: boolean | Appearance) => { + if (name === 'openAtStartup') { + setAutoLaunch(value as boolean); + } + + if (name === 'appearance') { + setAppearance(value as Appearance); + } + + setSettings({ ...settings, [name]: value }); + saveState(accounts, settings); + }, + [accounts, settings] + ); const isLoggedIn = useMemo(() => { return !!accounts.token || accounts.enterpriseAccounts.length > 0; @@ -52,22 +67,28 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const authCode = await authGitHub(); const { token } = await getToken(authCode.code); setAccounts({ ...accounts, token }); - }, []); + saveState({ ...accounts, token }, settings); + }, [accounts]); const logout = useCallback(() => { setAccounts(defaultAccounts); + clearState(); }, []); - useEffect(() => { - if (!accounts.token && !accounts.enterpriseAccounts) { - // Empty local storage - } else { - // Save accounts to local storage + const restoreSettings = useCallback(() => { + const existing = loadState(); + + if (existing.accounts) { + setAccounts({ ...defaultAccounts, ...existing.accounts }); } - }, [accounts]); + + if (existing.settings) { + setSettings({ ...defaultSettings, ...existing.settings }); + } + }, []); useEffect(() => { - // Reload settings + restoreSettings(); }, []); return ( diff --git a/src/context/Notifications.tsx b/src/context/Notifications.tsx index b3751c3b1..d051843af 100644 --- a/src/context/Notifications.tsx +++ b/src/context/Notifications.tsx @@ -83,7 +83,7 @@ export const NotificationsProvider = ({ .catch((error) => { setIsFetching(false); }); - }, [accounts]); + }, [accounts, settings]); return ( { - let dispatched = null; - const dispatch = settingsMiddleware()( - (actionAttempt) => (dispatched = actionAttempt) - ); - dispatch(action); - return dispatched; -}; - -describe('middleware/settings.js', () => { - it('should toggle the openAtStartup setting', () => { - spyOn(remote.app, 'setLoginItemSettings'); - - const action = { - type: actions.UPDATE_SETTING, - setting: 'openAtStartup', - value: true, - }; - - expect(dispatchWithStoreOf({}, action)).toEqual(action); - - expect(remote.app.setLoginItemSettings).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/middleware/settings.ts b/src/js/middleware/settings.ts deleted file mode 100644 index b0fdc03a4..000000000 --- a/src/js/middleware/settings.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UPDATE_SETTING } from '../actions'; -import { setAppearance } from '../utils/appearance'; -import { setAutoLaunch } from '../utils/comms'; - -export default () => (next) => (action) => { - switch (action.type) { - case UPDATE_SETTING: - if (action.setting === 'openAtStartup') { - setAutoLaunch(action.value); - } - - if (action.setting === 'appearance') { - setAppearance(action.value); - } - } - - return next(action); -}; diff --git a/src/js/utils/comms.ts b/src/js/utils/comms.ts index 80fb34554..96b5cdf5d 100644 --- a/src/js/utils/comms.ts +++ b/src/js/utils/comms.ts @@ -4,7 +4,7 @@ export function openExternalLink(url) { shell.openExternal(url); } -export function setAutoLaunch(value) { +export function setAutoLaunch(value: boolean) { remote.app.setLoginItemSettings({ openAtLogin: value, openAsHidden: value, diff --git a/src/js/utils/notifications.test.ts b/src/js/utils/notifications.test.ts index 79ad2828b..b1dfee212 100644 --- a/src/js/utils/notifications.test.ts +++ b/src/js/utils/notifications.test.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; -import { generateGitHubWebUrl } from '../utils/helpers'; +import { generateGitHubWebUrl } from '../../utils/helpers'; import { mockedGithubNotifications } from '../__mocks__/mockedData'; import * as comms from './comms'; import NotificationsUtils from '../utils/notifications'; diff --git a/src/js/utils/notifications.ts b/src/js/utils/notifications.ts index 37a6c3b0b..bd9f0d535 100644 --- a/src/js/utils/notifications.ts +++ b/src/js/utils/notifications.ts @@ -1,6 +1,6 @@ const { remote } = require('electron'); -import { generateGitHubWebUrl } from '../utils/helpers'; +import { generateGitHubWebUrl } from '../../utils/helpers'; import { reOpenWindow, openExternalLink } from '../utils/comms'; import { Notification } from '../../typesGithub'; diff --git a/src/js/utils/appearance.test.ts b/src/utils/appearance.test.ts similarity index 96% rename from src/js/utils/appearance.test.ts rename to src/utils/appearance.test.ts index 078c5021e..a9d7f0b05 100644 --- a/src/js/utils/appearance.test.ts +++ b/src/utils/appearance.test.ts @@ -1,4 +1,4 @@ -import { Appearance } from '../../types'; +import { Appearance } from '../types'; import { setAppearance } from './appearance'; import * as appearanceHelpers from './appearance'; diff --git a/src/js/utils/appearance.ts b/src/utils/appearance.ts similarity index 93% rename from src/js/utils/appearance.ts rename to src/utils/appearance.ts index 880c480ff..8eeab5fff 100644 --- a/src/js/utils/appearance.ts +++ b/src/utils/appearance.ts @@ -1,4 +1,4 @@ -import { Appearance } from '../../types'; +import { Appearance } from '../types'; export const setLightMode = () => document.querySelector('html').classList.remove('dark'); diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 000000000..b65d89f07 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,23 @@ +import { AuthState, SettingsState } from '../types'; +import { Constants } from './constants'; + +export const loadState = (): { + accounts?: AuthState; + settings?: SettingsState; +} => { + const existing = localStorage.getItem(Constants.STORAGE_KEY); + const { auth: accounts, settings } = (existing && JSON.parse(existing)) || {}; + return { accounts, settings }; +}; + +export const saveState = ( + accounts: AuthState, + settings: SettingsState +): void => { + const settingsString = JSON.stringify({ auth: accounts, settings }); + localStorage.setItem(Constants.STORAGE_KEY, settingsString); +}; + +export const clearState = (): void => { + localStorage.clear(); +}; From 14ad4fdc4b1cc5aabd3b231f0ceff34f0d7eb9b9 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Sun, 20 Dec 2020 12:58:59 +0000 Subject: [PATCH 06/25] chore: Use hook - useInterval --- src/components/Sidebar.tsx | 26 +-- src/context/Notifications.tsx | 17 +- src/hooks/useInterval.ts | 25 +++ src/js/actions/index.test.ts | 14 -- src/js/actions/index.ts | 370 +++++++++++----------------------- src/js/utils/notifications.ts | 2 +- 6 files changed, 163 insertions(+), 291 deletions(-) create mode 100644 src/hooks/useInterval.ts diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 51a629393..43ef0ade9 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -13,33 +13,11 @@ export const Sidebar: React.FC = () => { const history = useHistory(); const location = useLocation(); - const { accounts, isLoggedIn } = useContext(AppContext); + const { isLoggedIn } = useContext(AppContext); const { notifications, fetchNotifications } = useContext( NotificationsContext ); - useEffect(() => { - const iFrequency = 60000; - - const requestInterval = setInterval(() => { - refreshNotifications(); - }, iFrequency); - - return () => { - clearInterval(requestInterval); - }; - }, []); - - useEffect(() => { - fetchNotifications(); - }, [accounts]); - - const refreshNotifications = useCallback(() => { - if (isLoggedIn) { - fetchNotifications(); - } - }, [isLoggedIn]); - const onOpenBrowser = useCallback(() => { shell.openExternal(`https://github.com/${Constants.REPO_SLUG}`); }, []); @@ -82,7 +60,7 @@ export const Sidebar: React.FC = () => { <> @@ -124,7 +124,7 @@ export const LoginEnterpriseRoute: React.FC = () => { clientId: '', clientSecret: '', }} - onSubmit={loginEnterprise} + onSubmit={login} validate={validate} > {renderForm} diff --git a/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap b/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap index 67beca15d..cfe52f9e9 100644 --- a/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap +++ b/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/enterprise-login.js renders correctly 1`] = ` +exports[`routes/LoginEnterprise.js renders correctly 1`] = `
diff --git a/src/types.ts b/src/types.ts index 638e2d463..56563d6be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,9 +35,15 @@ export interface AccountNotifications { notifications: Notification[]; } -export interface AuthResponse { +export interface AuthOptions { hostname: string; - code: string; + clientId: string; + clientSecret: string; +} + +export interface AuthResponse { + authCode: string; + authOptions: AuthOptions; } export interface AuthTokenResponse { hostname: string; diff --git a/yarn.lock b/yarn.lock index 2c6e4ba94..f61107450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -258,13 +258,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.1", "@babel/template@^7.10.3", "@babel/template@^7.3.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" @@ -2841,7 +2834,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -3847,11 +3840,6 @@ lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -4605,7 +4593,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -4720,17 +4708,6 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-redux@=7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736" - integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA== - dependencies: - "@babel/runtime" "^7.12.1" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.13.1" - react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" @@ -4859,26 +4836,6 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -redux-mock-store@=1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" - integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA== - dependencies: - lodash.isplainobject "^4.0.6" - -redux-thunk@=2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" - integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== - -redux@=4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" - integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== - dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" - regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" @@ -5506,11 +5463,6 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" -symbol-observable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" From 5248d4c1eead77e8e2e9efdab861ed10334d374a Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 21 Dec 2020 20:22:56 +0000 Subject: [PATCH 17/25] fix: Fix more tests --- src/components/AccountNotifications.test.tsx | 8 +- src/components/Repository.test.tsx | 40 +++-- .../AccountNotifications.test.tsx.snap | 53 +++--- .../__snapshots__/Repository.test.tsx.snap | 13 +- src/routes/Login.test.tsx | 87 ++++------ src/routes/Login.tsx | 6 +- src/routes/Notifications.test.tsx | 79 +++------ src/routes/Settings.test.tsx | 160 ++++++++++-------- src/routes/__snapshots__/Login.test.tsx.snap | 2 +- .../__snapshots__/Notifications.test.tsx.snap | 6 +- .../__snapshots__/Settings.test.tsx.snap | 4 +- 11 files changed, 231 insertions(+), 227 deletions(-) diff --git a/src/components/AccountNotifications.test.tsx b/src/components/AccountNotifications.test.tsx index 0f5b06f6c..088fbfc4b 100644 --- a/src/components/AccountNotifications.test.tsx +++ b/src/components/AccountNotifications.test.tsx @@ -1,10 +1,12 @@ -import * as React from 'react'; -import * as TestRendener from 'react-test-renderer'; +import React from 'react'; +import TestRendener from 'react-test-renderer'; import { AccountNotifications } from './AccountNotifications'; import { mockedGithubNotifications } from '../__mocks__/mockedData'; -jest.mock('./repository'); +jest.mock('./Repository', () => ({ + RepositoryNotifications: () =>
Repository
, +})); describe('components/AccountNotifications.tsx', () => { it('should render itself (github.com with notifications)', () => { diff --git a/src/components/Repository.test.tsx b/src/components/Repository.test.tsx index 80751b5f7..8d9785940 100644 --- a/src/components/Repository.test.tsx +++ b/src/components/Repository.test.tsx @@ -1,33 +1,47 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; +import React from 'react'; +import TestRenderer from 'react-test-renderer'; import { render, fireEvent } from '@testing-library/react'; import { mockedGithubNotifications } from '../__mocks__/mockedData'; import { RepositoryNotifications } from './Repository'; +import { NotificationsContext } from '../context/Notifications'; const { shell } = require('electron'); -jest.mock('./notification'); +jest.mock('./Notification', () => ({ + NotificationItem: () =>
NotificationItem
, +})); + +describe('components/Repository.tsx', () => { + const markRepoNotifications = jest.fn(); -describe('components/repository.tsx', function () { const props = { hostname: 'github.com', repoName: 'manosim/gitify', repoNotifications: mockedGithubNotifications, - markRepoNotifications: jest.fn(), }; beforeEach(() => { + markRepoNotifications.mockReset(); + spyOn(shell, 'openExternal'); }); - it('should render itself & its children', function () { - const tree = TestRenderer.create(); + it('should render itself & its children', () => { + const tree = TestRenderer.create( + + + + ); expect(tree).toMatchSnapshot(); }); - it('should open the browser when clicking on the repo name', function () { - const { getByText } = render(); + it('should open the browser when clicking on the repo name', () => { + const { getByText } = render( + + + + ); fireEvent.click(getByText(props.repoName)); @@ -38,11 +52,15 @@ describe('components/repository.tsx', function () { }); it('should mark a repo as read', function () { - const { getByRole } = render(); + const { getByRole } = render( + + + + ); fireEvent.click(getByRole('button')); - expect(props.markRepoNotifications).toHaveBeenCalledWith( + expect(markRepoNotifications).toHaveBeenCalledWith( 'manosim/notifications-test', 'github.com' ); diff --git a/src/components/__snapshots__/AccountNotifications.test.tsx.snap b/src/components/__snapshots__/AccountNotifications.test.tsx.snap index eb75618a1..18c3181df 100644 --- a/src/components/__snapshots__/AccountNotifications.test.tsx.snap +++ b/src/components/__snapshots__/AccountNotifications.test.tsx.snap @@ -1,32 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/AccountNotifications.tsx should render itself (github.com with notifications) 1`] = ` -
- github.com -
+ github.com +
+ viewBox="0 0 16 16" + width={20} + /> +
, +
+ Repository +
, +] `; exports[`components/AccountNotifications.tsx should render itself (github.com without notifications) 1`] = ` diff --git a/src/components/__snapshots__/Repository.test.tsx.snap b/src/components/__snapshots__/Repository.test.tsx.snap index b00ba7351..2bf647a99 100644 --- a/src/components/__snapshots__/Repository.test.tsx.snap +++ b/src/components/__snapshots__/Repository.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/repository.tsx should render itself & its children 1`] = ` +exports[`components/Repository.tsx should render itself & its children 1`] = ` Array [
, -
, +
+
+ NotificationItem +
+
+ NotificationItem +
+
, ] `; diff --git a/src/routes/Login.test.tsx b/src/routes/Login.test.tsx index eb94dbb72..290157ff5 100644 --- a/src/routes/Login.test.tsx +++ b/src/routes/Login.test.tsx @@ -1,41 +1,30 @@ -// @ts-nocheck -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { MemoryRouter } from 'react-router'; +import React from 'react'; +import TestRenderer from 'react-test-renderer'; +import { Router } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { render, fireEvent } from '@testing-library/react'; -const { ipcRenderer, remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; +const { ipcRenderer } = require('electron'); +import { AppContext } from '../context/App'; import { LoginRoute } from './login'; -import * as helpers from '../utils/helpers'; -describe('routes/login.tsx', () => { - const props = { - dispatch: jest.fn(), - isEitherLoggedIn: false, - history: { - goBack: jest.fn(), - push: jest.fn(), - }, - }; +describe('routes/Login.tsx', () => { + const history = createMemoryHistory(); + const pushMock = jest.spyOn(history, 'push'); + const replaceMock = jest.spyOn(history, 'replace'); beforeEach(function () { - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); + pushMock.mockReset(); + spyOn(ipcRenderer, 'send'); - props.dispatch.mockReset(); - props.history.push.mockReset(); }); it('should render itself & its children', () => { - const caseProps = { - ...props, - }; - const tree = TestRenderer.create( - + ); @@ -43,53 +32,37 @@ describe('routes/login.tsx', () => { }); it('should redirect to notifications once logged in', () => { - const caseProps = { - ...props, - }; - const { rerender } = render( - - - + + + + + ); rerender( - - - + + + + + ); expect(ipcRenderer.send).toHaveBeenCalledTimes(1); expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window'); - }); - - it('should call the authGitHub helper when pressing the login button', () => { - spyOn(helpers, 'authGithub'); - const caseProps = { - ...props, - }; - - const { getByLabelText } = render( - - - - ); - - fireEvent.click(getByLabelText('Login with GitHub')); - - expect(helpers.authGithub).toHaveBeenCalledTimes(1); + expect(replaceMock).toHaveBeenCalledTimes(1); }); it('should navigate to login with github enterprise', () => { const { getByLabelText } = render( - - - + + + ); fireEvent.click(getByLabelText('Login with GitHub Enterprise')); - expect(props.history.push).toHaveBeenCalledTimes(1); - expect(props.history.push).toHaveBeenCalledWith('/enterpriselogin'); + expect(pushMock).toHaveBeenCalledTimes(1); + expect(pushMock).toHaveBeenCalledWith('/enterpriselogin'); }); }); diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx index d29a7bf66..61aa39367 100644 --- a/src/routes/Login.tsx +++ b/src/routes/Login.tsx @@ -1,12 +1,12 @@ const { ipcRenderer } = require('electron'); import React, { useCallback, useContext, useEffect } from 'react'; -import { RouteComponentProps, useHistory } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { AppContext } from '../context/App'; import { Logo } from '../components/Logo'; -export const LoginRoute: React.FC = (props) => { +export const LoginRoute: React.FC = () => { const history = useHistory(); const { isLoggedIn, login } = useContext(AppContext); @@ -46,7 +46,7 @@ export const LoginRoute: React.FC = (props) => { ; + }; + + const { getByText } = customRender(); + + fetchNotificationsMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(fetchNotificationsMock).toHaveBeenCalled(); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(1); + }); + + it('should call markNotification', async () => { + const TestComponent = () => { + const { markNotification } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + markNotificationMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(markNotificationMock).toHaveBeenCalledTimes(1); + expect(markNotificationMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + '123-456', + 'github.com' + ); + }); + + it('should call unsubscribeNotification', async () => { + const TestComponent = () => { + const { unsubscribeNotification } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + unsubscribeNotificationMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(unsubscribeNotificationMock).toHaveBeenCalledTimes(1); + expect(unsubscribeNotificationMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + '123-456', + 'github.com' + ); + }); + + it('should call markRepoNotifications', async () => { + const TestComponent = () => { + const { markRepoNotifications } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + markRepoNotificationsMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(markRepoNotificationsMock).toHaveBeenCalledTimes(1); + expect(markRepoNotificationsMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + 'manosim/gitify', + 'github.com' + ); + }); + }); + + it('should call updateSetting', async () => { + const saveStateMock = jest.spyOn(storage, 'saveState'); + + const TestComponent = () => { + const { updateSetting } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(saveStateMock).toHaveBeenCalled(); + expect(saveStateMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + { + appearance: 'SYSTEM', + markOnClick: false, + openAtStartup: false, + participating: true, + playSound: true, + showNotifications: true, + } + ); + }); + + it('should call updateSetting and set auto launch(openAtStartup)', async () => { + const setAutoLaunchMock = jest.spyOn(comms, 'setAutoLaunch'); + const saveStateMock = jest.spyOn(storage, 'saveState'); + + const TestComponent = () => { + const { updateSetting } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(setAutoLaunchMock).toHaveBeenCalled(); + expect(setAutoLaunchMock).toHaveBeenCalledWith(true); + expect(saveStateMock).toHaveBeenCalled(); + expect(saveStateMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + { + appearance: 'SYSTEM', + markOnClick: false, + openAtStartup: true, + participating: false, + playSound: true, + showNotifications: true, + } + ); + }); + + it('should check isLoggedIn', async () => { + const TestComponent = () => { + const { updateSetting } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(setAutoLaunchMock).toHaveBeenCalled(); + expect(setAutoLaunchMock).toHaveBeenCalledWith(true); + expect(saveStateMock).toHaveBeenCalled(); + expect(saveStateMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + { + appearance: 'SYSTEM', + markOnClick: false, + openAtStartup: true, + participating: false, + playSound: true, + showNotifications: true, + } + ); + }); +}); diff --git a/src/context/App.tsx b/src/context/App.tsx index cc079fc6a..13dfa37ef 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -140,25 +140,25 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }, []); const fetchNotificationsWithAccounts = useCallback( - async () => fetchNotifications(accounts, settings), + async () => await fetchNotifications(accounts, settings), [accounts, settings, notifications] ); const markNotificationWithAccounts = useCallback( async (id: string, hostname: string) => - markNotification(accounts, id, hostname), + await markNotification(accounts, id, hostname), [accounts, notifications] ); const unsubscribeNotificationWithAccounts = useCallback( async (id: string, hostname: string) => - unsubscribeNotification(accounts, id, hostname), + await unsubscribeNotification(accounts, id, hostname), [accounts, notifications] ); const markRepoNotificationsWithAccounts = useCallback( async (repoSlug: string, hostname: string) => - markRepoNotifications(accounts, repoSlug, hostname), + await markRepoNotifications(accounts, repoSlug, hostname), [accounts, notifications] ); From 4dc60594ae66b5ccb26db51bb00ec264ff69cdcb Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Thu, 24 Dec 2020 15:24:25 +0000 Subject: [PATCH 24/25] chore: Clean Up --- src/context/App.test.tsx | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx index 5487a2329..fd2587c71 100644 --- a/src/context/App.test.tsx +++ b/src/context/App.test.tsx @@ -234,37 +234,4 @@ describe('context/App.tsx', () => { } ); }); - - it('should check isLoggedIn', async () => { - const TestComponent = () => { - const { updateSetting } = useContext(AppContext); - - return ( - - ); - }; - - const { getByText } = customRender(); - - act(() => { - fireEvent.click(getByText('Test Case')); - }); - - expect(setAutoLaunchMock).toHaveBeenCalled(); - expect(setAutoLaunchMock).toHaveBeenCalledWith(true); - expect(saveStateMock).toHaveBeenCalled(); - expect(saveStateMock).toHaveBeenCalledWith( - { enterpriseAccounts: [], token: null }, - { - appearance: 'SYSTEM', - markOnClick: false, - openAtStartup: true, - participating: false, - playSound: true, - showNotifications: true, - } - ); - }); }); From 49fef73189c0491d594af25b239e2205aee8b109 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Thu, 24 Dec 2020 17:13:47 +0000 Subject: [PATCH 25/25] chore: Rename Notification component to NotificationRow To avoid confusion with native notifications --- ...otification.test.tsx => NotificationRow.test.tsx} | 12 ++++++------ .../{Notification.tsx => NotificationRow.tsx} | 2 +- src/components/Repository.test.tsx | 4 ++-- src/components/Repository.tsx | 4 ++-- ...n.test.tsx.snap => NotificationRow.test.tsx.snap} | 0 .../__snapshots__/Repository.test.tsx.snap | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) rename src/components/{Notification.test.tsx => NotificationRow.test.tsx} (91%) rename src/components/{Notification.tsx => NotificationRow.tsx} (98%) rename src/components/__snapshots__/{Notification.test.tsx.snap => NotificationRow.test.tsx.snap} (100%) diff --git a/src/components/Notification.test.tsx b/src/components/NotificationRow.test.tsx similarity index 91% rename from src/components/Notification.test.tsx rename to src/components/NotificationRow.test.tsx index e8feb22ab..b38da7a8f 100644 --- a/src/components/Notification.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -6,7 +6,7 @@ const { shell } = require('electron'); import { AppContext } from '../context/App'; import { mockedSingleNotification } from '../__mocks__/mockedData'; -import { NotificationItem } from './Notification'; +import { NotificationRow } from './NotificationRow'; import { mockSettings } from '../__mocks__/mock-state'; describe('components/Notification.js', () => { @@ -22,7 +22,7 @@ describe('components/Notification.js', () => { hostname: 'github.com', }; - const tree = TestRenderer.create(); + const tree = TestRenderer.create(); expect(tree).toMatchSnapshot(); }); @@ -41,7 +41,7 @@ describe('components/Notification.js', () => { markNotification, }} > - + ); @@ -64,7 +64,7 @@ describe('components/Notification.js', () => { markNotification, }} > - + ); @@ -86,7 +86,7 @@ describe('components/Notification.js', () => { value={{ settings: { ...mockSettings, markOnClick: false } }} > - + ); @@ -106,7 +106,7 @@ describe('components/Notification.js', () => { const { getByLabelText } = render( - + ); diff --git a/src/components/Notification.tsx b/src/components/NotificationRow.tsx similarity index 98% rename from src/components/Notification.tsx rename to src/components/NotificationRow.tsx index fb00931f0..12d955dc4 100644 --- a/src/components/Notification.tsx +++ b/src/components/NotificationRow.tsx @@ -14,7 +14,7 @@ interface IProps { notification: Notification; } -export const NotificationItem: React.FC = ({ +export const NotificationRow: React.FC = ({ notification, hostname, }) => { diff --git a/src/components/Repository.test.tsx b/src/components/Repository.test.tsx index 9288e1adf..ce5d87c65 100644 --- a/src/components/Repository.test.tsx +++ b/src/components/Repository.test.tsx @@ -8,8 +8,8 @@ import { RepositoryNotifications } from './Repository'; const { shell } = require('electron'); -jest.mock('./Notification', () => ({ - NotificationItem: () =>
NotificationItem
, +jest.mock('./NotificationRow', () => ({ + NotificationRow: () =>
NotificationRow
, })); describe('components/Repository.tsx', () => { diff --git a/src/components/Repository.tsx b/src/components/Repository.tsx index 628e6ac07..14ee53c2b 100644 --- a/src/components/Repository.tsx +++ b/src/components/Repository.tsx @@ -6,7 +6,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { AppContext } from '../context/App'; import { Notification } from '../typesGithub'; -import { NotificationItem } from './Notification'; +import { NotificationRow } from './NotificationRow'; interface IProps { hostname: string; @@ -55,7 +55,7 @@ export const RepositoryNotifications: React.FC = ({ {repoNotifications.map((obj) => ( - ,
- NotificationItem + NotificationRow
- NotificationItem + NotificationRow
, ]