diff --git a/.eslintrc.json b/.eslintrc.json
index 4338d1d8..243f8f5c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -21,7 +21,7 @@
"ecmaVersion": 11,
"sourceType": "module"
},
- "plugins": ["react"],
+ "plugins": ["react", "@emotion"],
"rules": {
"class-methods-use-this": "off",
"comma-dangle": "off",
diff --git a/common/config.js b/common/config.js
deleted file mode 100644
index bb557da4..00000000
--- a/common/config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const env = require('./env');
-
-const config = { ...env };
-
-module.exports = config;
diff --git a/common/request.js b/common/request.js
index 35ddc5ad..42cd4e99 100644
--- a/common/request.js
+++ b/common/request.js
@@ -1,7 +1,5 @@
const axios = require('axios');
-const storage = require('./storage');
-
let instance;
const TWENTY_SECONDS = 20000;
@@ -43,13 +41,6 @@ const getInstance = () => instance;
const reset = () => { instance = undefined; };
-if (storage.user.isDefined()) {
- const { api_key, redmineEndpoint } = storage.user.get();
- if (api_key && redmineEndpoint) {
- initialize(redmineEndpoint, api_key);
- }
-}
-
const handleReject = (error) => {
// if this request was not cancelled
if (!axios.isCancel(error)) {
diff --git a/common/storage.js b/common/storage.js
deleted file mode 100644
index c93edaeb..00000000
--- a/common/storage.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const os = require('os');
-const Store = require('electron-store');
-const isDev = require('electron-is-dev');
-const { ENCRYPTION_KEY } = require('./env');
-
-const storage = new Store({
- name: isDev ? 'config-dev' : 'config',
- encryptionKey: ENCRYPTION_KEY
-});
-
-const api = {
- settings: {
- get: () => storage.get('settings', { os: os.platform }),
- isDefined: () => storage.has('settings'),
- set: (data) => storage.set('settings', data)
- },
- user: {
- get: () => storage.get('user'),
- isDefined: () => storage.has('user'),
- set: (data) => storage.set('user', data)
- }
-};
-
-// TODO: save os.platform into the storage on start
-
-module.exports = api;
diff --git a/common/env.js b/main/env.js
similarity index 100%
rename from common/env.js
rename to main/env.js
diff --git a/main/exceptionCatcher.js b/main/exceptionCatcher.js
index 7986210a..8d69c1a2 100644
--- a/main/exceptionCatcher.js
+++ b/main/exceptionCatcher.js
@@ -5,7 +5,7 @@ const isDev = require('electron-is-dev');
const { app, dialog, clipboard } = require('electron');
const logger = require('electron-log');
-const { report } = require('../common/reporter');
+const { report } = require('./handlers/errorHandler');
const config = {
showDialog: !isDev,
diff --git a/common/reporter.js b/main/handlers/errorHandler.js
similarity index 100%
rename from common/reporter.js
rename to main/handlers/errorHandler.js
diff --git a/main/index.js b/main/index.js
index e304e338..d1f56f57 100644
--- a/main/index.js
+++ b/main/index.js
@@ -12,7 +12,7 @@ const electronUtils = require('electron-util');
const isDev = require('electron-is-dev');
const logger = require('electron-log');
-const storage = require('../common/storage');
+const storage = require('./storage');
const { redmineClient } = require('./redmine');
const Tray = require('./tray');
@@ -344,32 +344,53 @@ const initialize = () => {
notification.show();
});
- ipcMain.on('menu', (event, { settings }) => {
+ ipcMain.on('menu', (event, message) => {
+ const { settings } = JSON.stringify(message);
generateMenu({ settings });
});
- ipcMain.on('storage', (event, message) => {
- const { action, data } = JSON.parse(message);
- if (action === 'read') {
- event.reply('storage', storage.settings.get());
- } else if (action === 'save') {
- storage.settings.set(data);
- } else {
- throw new Error('Unable to process the requested action', action);
- }
- });
+ storage.initializeSessionEvents(ipcMain);
ipcMain.on('request', async (event, message) => {
- const { payload, config, id } = JSON.parse(message);
- console.log('Received a request for query', { payload, config });
+ const { payload, id } = JSON.parse(message);
+ console.log('Received a request for query', { payload });
+
if (!redmineClient.isInitialized()) {
- redmineClient.initialize(config);
+ const error = new Error('Unauthorized');
+ error.status = 401;
+ event.reply(`response:${id}`, { success: false, error });
+ return;
}
const response = await redmineClient.send(payload);
event.reply(`response:${id}`, response);
});
+
+ ipcMain.on('system-request', async (event, message) => {
+ const { action, payload, id } = JSON.parse(message);
+ console.log(`Received system request for ${action}`, { payload });
+
+ switch (action.toLowerCase()) {
+ case 'login': {
+ const response = await redmineClient.initialize({
+ endpoint: payload.endpoint,
+ token: payload.token,
+ headers: payload.headers
+ });
+ event.reply(`system-response:${id}`, response);
+ break;
+ }
+ case 'logout': {
+ redmineClient.reset();
+ await storage.resetActiveSession();
+ event.reply(`system-response:${id}`, { success: true });
+ break;
+ }
+ default:
+ break;
+ }
+ });
};
app.on('certificate-error', (event, webContents, _url, error, certificate, callback) => {
@@ -391,15 +412,8 @@ app.once('ready', () => {
}
// eslint-disable-next-line global-require
- const config = require('../common/config');
+ const config = require('./env');
PORT = config.PORT;
- // eslint-disable-next-line global-require
- require('../common/request'); // to initialize from storage
-
- if (!storage.settings.isDefined()) {
- // sets defaul value that storage.settings.get returns as a fallback
- storage.settings.set(storage.settings.get());
- }
initialize();
generateMenu();
@@ -410,6 +424,7 @@ app.once('ready', () => {
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
+ storage.disposeSessionEvents(ipcMain);
app.quit();
}
});
diff --git a/main/redmine.js b/main/redmine.js
index ab0a2f91..c4ecf0ac 100644
--- a/main/redmine.js
+++ b/main/redmine.js
@@ -1,26 +1,76 @@
const got = require('got');
const { transform } = require('./transformers/index');
-const createRequestClient = (initConfig = {}) => {
- let isInitialized = false;
+const createRequestClient = () => {
let instance;
-
- const initialize = (config) => {
- isInitialized = Boolean(config.endpoint && config.token);
- instance = got.extend({
- prefixUrl: config.endpoint,
- headers: { 'X-Redmine-API-Key': config.token },
- responseType: 'json'
- });
- };
+ let isInitialized;
const handleUnsuccessfulRequest = (error) => ({
success: false,
error
});
+ const initialize = async (data) => {
+ if (isInitialized) {
+ reset();
+ }
+
+ const route = 'users/current.json';
+
+ const configuration = data.token
+ ? {
+ prefixUrl: data.endpoint,
+ headers: { 'X-Redmine-API-Key': data.token },
+ responseType: 'json'
+ }
+ : {
+ prefixUrl: data.endpoint,
+ headers: {
+ Authorization: data.headers.Authorization
+ },
+ responseType: 'json'
+ };
+
+ try {
+ const response = await got(route, {
+ method: 'GET',
+ ...configuration
+ });
+
+ console.log(response.statusCode, response.body);
+
+ const loginSuccess = response.statusCode === 200;
+ isInitialized = loginSuccess;
+
+ if (loginSuccess) {
+ const payload = transform(route, response.body);
+
+ instance = got.extend({
+ ...configuration,
+ headers: {
+ 'X-Redmine-API-Key': payload.token
+ }
+ });
+
+ return {
+ success: true,
+ payload
+ };
+ }
+
+ return handleUnsuccessfulRequest(response.body);
+ } catch (error) {
+ return handleUnsuccessfulRequest(error);
+ }
+ };
+
+ const reset = () => {
+ isInitialized = false;
+ instance = undefined;
+ };
+
const send = async (data) => {
- if (!instance && !data.headers?.Authorization) {
+ if (!isInitialized) {
throw new Error('Http client is not initialized.');
}
@@ -34,12 +84,8 @@ const createRequestClient = (initConfig = {}) => {
console.log(response.statusCode, response.body);
if (response.statusCode === 200) {
- if (!isInitialized && data.route === 'users/current.json' && response.body.user?.api_key) {
- initialize({ endpoint: initConfig.endpoint, token: response.body.user?.api_key });
- }
-
return {
- data: transform(data.route, response.body),
+ payload: transform(data.route, response.body),
success: true,
};
}
@@ -50,12 +96,11 @@ const createRequestClient = (initConfig = {}) => {
}
};
- initialize(initConfig);
-
return {
initialize,
isInitialized: () => isInitialized,
- send
+ send,
+ reset
};
};
diff --git a/main/storage.js b/main/storage.js
new file mode 100644
index 00000000..fd2406ab
--- /dev/null
+++ b/main/storage.js
@@ -0,0 +1,144 @@
+const os = require('os');
+const Store = require('electron-store');
+const isDev = require('electron-is-dev');
+const crypto = require('crypto');
+
+const { ENCRYPTION_KEY } = require('./env');
+
+const TOKEN_FALLBACK = '';
+
+const storage = new Store({
+ name: isDev ? 'config-dev' : 'config',
+ encryptionKey: ENCRYPTION_KEY
+});
+
+const hashToken = (token) => crypto.createHash('sha256').update(token || TOKEN_FALLBACK, 'utf8').digest('hex');
+
+console.log(JSON.stringify(storage.get('activeSession'), null, 2));
+console.log(JSON.stringify(storage.get('persistedSessions'), null, 2));
+
+const upsertSavedSession = (persistedSessions, activeSession) => {
+ const savedActiveSessionIndex = persistedSessions.findIndex((session) => session.hash === activeSession.hash);
+
+ if (savedActiveSessionIndex !== -1) {
+ const persistedSessionsCopy = [...persistedSessions];
+ persistedSessionsCopy[savedActiveSessionIndex] = activeSession;
+ storage.set('persistedSessions', persistedSessionsCopy);
+ } else {
+ const updatedPersistedSessions = [...persistedSessions, activeSession];
+ storage.set('persistedSessions', updatedPersistedSessions);
+ }
+};
+
+const getActiveSession = () => {
+ const activeSession = storage.get('activeSession');
+ if (activeSession) {
+ return {
+ ...activeSession,
+ platforn: os.platform
+ };
+ }
+
+ return activeSession;
+};
+
+const getAllSettings = () => {
+ const activeSession = getActiveSession();
+ const persistedSessions = storage.get('persistedSessions', []);
+ return {
+ activeSession,
+ persistedSessions
+ };
+};
+
+const getSession = (token) => {
+ const { activeSession, persistedSessions } = getAllSettings();
+ const hash = hashToken(token);
+
+ if (hash === activeSession?.hash) {
+ return getActiveSession();
+ }
+
+ const targetSession = persistedSessions.find(session => session.hash === hash);
+
+ return targetSession;
+};
+
+const resetActiveSession = () => {
+ storage.delete('activeSession');
+};
+
+const saveSession = (session) => {
+ const sessionHash = hashToken(session.token);
+
+ const sessionObject = {
+ hash: sessionHash,
+ user: {
+ id: session.user.id,
+ firstName: session.user.firstName,
+ lastName: session.user.lastName,
+ createdOn: session.user.createdOn
+ },
+ endpoint: session.endpoint,
+ settings: {
+ ...session.settings
+ }
+ };
+
+ storage.set('activeSession', sessionObject);
+ upsertSavedSession(storage.get('persistedSessions', []), sessionObject);
+};
+
+const eventHandlers = (event, message) => {
+ const { action, payload, id } = JSON.parse(message);
+
+ console.log('Received session event', message);
+
+ switch (action) {
+ case 'READ': {
+ const session = getSession(payload.token);
+
+ if (!session) {
+ event.reply(`session-response:${id}`, {
+ success: false
+ });
+ break;
+ }
+
+ const { hash, ...sessionWithoutHash } = session;
+
+ event.reply(`session-response:${id}`, {
+ success: true,
+ payload: sessionWithoutHash
+ });
+ break;
+ }
+ case 'SAVE': {
+ saveSession(payload);
+ event.reply(`session-response:${id}`, { success: true });
+ break;
+ }
+ default: {
+ event.reply(`session-response:${id}`, {
+ success: false,
+ error: new Error('Unable to process the requested action', action)
+ });
+ }
+ }
+};
+
+const initializeSessionEvents = (ipcMain) => {
+ ipcMain.on('session-request', eventHandlers);
+};
+
+const disposeSessionEvents = (ipcMain) => {
+ ipcMain.off('session-request', eventHandlers);
+};
+
+module.exports = {
+ getActiveSession,
+ getAllSettings,
+ resetActiveSession,
+ initializeSessionEvents,
+ disposeSessionEvents
+};
diff --git a/main/transformers/users.js b/main/transformers/users.js
index cd205c0f..46d62bdf 100644
--- a/main/transformers/users.js
+++ b/main/transformers/users.js
@@ -4,9 +4,16 @@ const transform = (route, responseBody) => {
switch (route) {
case 'users/current.json': {
const {
- _login, _admin, _api_key, ...userPayload
+ // eslint-disable-next-line camelcase
+ login, admin, last_login_on, ...userPayload
} = user;
- return userPayload;
+ return {
+ id: userPayload.id,
+ firstName: userPayload.firstname,
+ lastName: userPayload.lastname,
+ createdOn: userPayload.created_on,
+ token: userPayload.api_key
+ };
}
default:
return responseBody;
diff --git a/package.json b/package.json
index d3422d27..7cc01a49 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
+ "@emotion/eslint-plugin": "^11.5.0",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.0",
"@types/hapi__joi": "^17.1.7",
@@ -138,6 +139,7 @@
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
+ "@emotion/react": "^11.5.0",
"@hapi/joi": "^17.1.0",
"axios": "^0.19.2",
"clean-stack": "^2.2.0",
@@ -148,6 +150,7 @@
"electron-updater": "^4.2.2",
"electron-util": "^0.14.0",
"emotion": "^11.0.0",
+ "emotion-theming": "^11.0.0",
"ensure-error": "^2.0.0",
"formik": "^2.1.4",
"got": "^11.8.2",
diff --git a/render/App.jsx b/render/App.jsx
index 0fb67e07..0cac44c4 100644
--- a/render/App.jsx
+++ b/render/App.jsx
@@ -1,7 +1,7 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
-import { Route, Switch } from 'react-router-dom';
+import { Route, Switch, useHistory } from 'react-router-dom';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import cleanStack from 'clean-stack';
@@ -11,7 +11,9 @@ import { ipcRenderer } from "electron";
import AppView from './views/AppView';
import LoginView from './views/LoginView';
import Notification from './components/Notification';
-import actions from './actions';
+import { useOvermindActions } from './store';
+import { getStoredToken } from './helpers/utils';
+import { LoadingOverlay } from './components/LoadingOverlay';
toast.configure({
autoClose: 3000,
@@ -22,6 +24,11 @@ toast.configure({
});
const Routes = ({ dispatch }) => {
+ const [isReady, setIsReady] = useState(false);
+
+ const actions = useOvermindActions();
+ const history = useHistory();
+
const handleRejection = useCallback(debounce((event) => {
event.preventDefault();
if (event.reason) {
@@ -71,10 +78,27 @@ const Routes = ({ dispatch }) => {
};
}, [handleError, handleRejection, settingsEventHandler]);
+ useEffect(() => {
+ const restoreLastSession = async () => {
+ const response = await actions.settings.restore(getStoredToken());
+ if (response.success) {
+ history.replace('/app');
+ }
+
+ setIsReady(true);
+ };
+
+ restoreLastSession();
+ }, []);
+
+ if (!isReady) {
+ return ;
+ }
+
return (
- } />
+ } />
);
};
diff --git a/render/about/AboutPage.jsx b/render/about/AboutPage.jsx
index 9043058b..d1c84850 100644
--- a/render/about/AboutPage.jsx
+++ b/render/about/AboutPage.jsx
@@ -9,7 +9,7 @@ import LogoIcon from '../../assets/icon.png';
import DragArea from '../components/DragArea';
import Link from '../components/Link';
-import { report } from '../../common/reporter';
+// import { report } from '../../common/reporter';
import License from './License';
const FlexBox = styled.div`
@@ -124,7 +124,7 @@ const Paragraph = styled.p`
`;
class AboutPage extends Component {
- onReportButtonClick = () => report()
+ onReportButtonClick = () => { /* noop */ } // report()
render() {
return (
diff --git a/render/actions/__tests__/helper.spec.js b/render/actions/__tests__/helper.spec.js
index 087284d0..bd1feed1 100644
--- a/render/actions/__tests__/helper.spec.js
+++ b/render/actions/__tests__/helper.spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import request, {
- IssueFilter, notify, login, logout
+ IssueFilter, notify
} from '../helper';
import axios from '../../../common/request';
@@ -23,8 +23,6 @@ describe('Helper module', () => {
it('should expose all the necesary items', () => {
expect(request).toBeTruthy();
expect(notify).toBeTruthy();
- expect(login).toBeTruthy();
- expect(logout).toBeTruthy();
expect(IssueFilter).toBeTruthy();
});
@@ -138,11 +136,6 @@ describe('Helper module', () => {
axiosMock.onGet('/user').replyOnce(() => Promise.resolve([200, response]));
- await expect(login({
- redmineEndpoint: 'redmine.test.com',
- url: '/user'
- })).resolves.toEqual({ data: response });
-
expect(axios.getInstance()).toBeTruthy();
expect(axiosMock.history.get.length).toBe(1);
expect(axiosMock.history.get[0].url).toBe('/user');
@@ -168,32 +161,11 @@ describe('Helper module', () => {
axiosInstanceMock.restore();
axios.reset();
});
-
- it('should throw if failed', async () => {
- expect(axios.getInstance()).toBe(undefined);
-
- const error = new Error('Test response error');
- axiosMock.onGet('/user').replyOnce(() => Promise.reject(error));
-
- try {
- await login({
- redmineEndpoint: 'redmine.test.com',
- url: '/user'
- });
- } catch (e) {
- expect(e).toBeInstanceOf(Error);
- expect(e.message).toBe('Error (Test response error)');
- }
-
- expect(axios.getInstance()).toBe(undefined);
- expect(axiosMock.history.get.length).toBe(1);
- });
});
describe('Logout action', () => {
it('should reset the axios instnace', () => {
const spy = jest.spyOn(axios, 'reset');
- logout();
expect(spy).toHaveBeenCalled();
});
});
diff --git a/render/actions/__tests__/user.actions.spec.js b/render/actions/__tests__/user.actions.spec.js
deleted file mode 100644
index ad4ab13d..00000000
--- a/render/actions/__tests__/user.actions.spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-
-import * as userActions from '../user.actions';
-import { notify } from '../helper';
-import settingsActions from '../settings.actions';
-import axios from '../../../common/request';
-
-const redmineEndpoint = 'redmine.test.com';
-const token = 'multipass';
-let axiosInstanceMock;
-let axiosMock;
-
-describe('User actions', () => {
- beforeAll(() => {
- axiosMock = new MockAdapter(axios.default);
- axios.initialize(redmineEndpoint, token);
- axiosInstanceMock = new MockAdapter(axios.getInstance());
- });
-
- afterAll(() => {
- axiosInstanceMock.restore();
- axiosMock.restore();
- axios.reset();
- });
-
- afterEach(() => {
- axiosInstanceMock.reset();
- axiosMock.reset();
- });
-
- it('should expose all the necessary actions', () => {
- expect(userActions).toBeTruthy();
- expect(userActions.USER_LOGIN).toBeTruthy();
- expect(userActions.USER_LOGOUT).toBeTruthy();
- expect(userActions.USER_GET_CURRENT).toBeTruthy();
-
- expect(userActions.default.checkLogin).toBeTruthy();
- expect(userActions.default.getCurrent).toBeTruthy();
- expect(userActions.default.logout).toBeTruthy();
- });
-
- describe('checkLogin action', () => {
- it('should make request and return the response with correct actions', async () => {
- const response = {
- user: {}
- };
- const username = 'usernae';
- const password = 'password';
-
- const settingsRestoreSpy = jest.spyOn(settingsActions, 'restore');
- const dispatch = jest.fn();
- axiosMock.onGet('/users/current.json').replyOnce(() => Promise.resolve([200, response]));
-
- await userActions.default.checkLogin({
- username,
- password,
- redmineEndpoint
- })(dispatch);
- expect(axiosMock.history.get.length).toBe(1);
- expect(axiosMock.history.get[0].url).toBe('/users/current.json');
- expect(axiosMock.history.get[0].headers.Authorization).toBe(`Basic ${btoa(`${username}:${password}`)}`);
- expect(dispatch).toHaveBeenCalledTimes(3);
- expect(dispatch).toHaveBeenCalledWith(notify.start(userActions.USER_LOGIN));
- expect(dispatch).toHaveBeenCalledWith(notify.ok(userActions.USER_LOGIN, {
- user: {
- ...response.user,
- redmineEndpoint
- }
- }));
- expect(settingsRestoreSpy).toHaveBeenCalled();
-
- // resetting, because checkLogin creates a new instance of axios if fullfilled
- axiosInstanceMock.restore();
- axios.reset();
- axios.initialize(redmineEndpoint, token);
- axiosInstanceMock = new MockAdapter(axios.getInstance());
- settingsRestoreSpy.mockRestore();
- });
-
- it('should pass the error further with dispatch', async () => {
- const response = new Error('Whoops');
- response.status = 500;
- const username = 'username';
- const password = 'password';
- const dispatch = jest.fn();
- axiosMock.onGet('/users/current.json').replyOnce(() => Promise.reject(response));
- await userActions.default.checkLogin({ redmineEndpoint, username, password })(dispatch);
- expect(axiosMock.history.get.length).toBe(1);
- expect(axiosMock.history.get[0].url).toBe('/users/current.json');
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith(notify.start(userActions.USER_LOGIN));
- expect(dispatch).toHaveBeenCalledWith(
- notify.nok(userActions.USER_LOGIN, new Error(`Error ${response.status} (${response.message})`))
- );
- });
- });
-
- describe('getCurrent action', () => {
- it('should make request and return the response with correct actions', async () => {
- const response = {
- user: {}
- };
- const dispatch = jest.fn();
- expect(axios.getInstance());
- axiosInstanceMock.onGet('/users/current.json').replyOnce(() => Promise.resolve([200, response]));
- await userActions.default.getCurrent()(dispatch);
- expect(axiosInstanceMock.history.get.length).toBe(1);
- expect(axiosInstanceMock.history.get[0].url).toBe('/users/current.json');
- expect(axiosInstanceMock.history.get[0].headers['X-Redmine-API-Key']).toBe(token);
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith(notify.start(userActions.USER_GET_CURRENT));
- expect(dispatch).toHaveBeenCalledWith(notify.ok(userActions.USER_GET_CURRENT, response));
- });
-
- it('should pass the error further with dispatch', async () => {
- const response = new Error('Whoops');
- response.status = 500;
- const dispatch = jest.fn();
- axiosInstanceMock.onGet('/users/current.json').replyOnce(() => Promise.reject(response));
- await userActions.default.getCurrent()(dispatch);
- expect(axiosInstanceMock.history.get.length).toBe(1);
- expect(axiosInstanceMock.history.get[0].url).toBe('/users/current.json');
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith(notify.start(userActions.USER_GET_CURRENT));
- expect(dispatch).toHaveBeenCalledWith(
- notify.nok(userActions.USER_GET_CURRENT, new Error(`Error ${response.status} (${response.message})`))
- );
- });
- });
-
- describe('logout action', () => {
- it('should make request and return the response with correct actions', () => {
- const resetSpy = jest.spyOn(axios, 'reset');
- const dispatch = jest.fn();
- const settingsBackupSpy = jest.spyOn(settingsActions, 'backup');
- userActions.default.logout()(dispatch);
- expect(dispatch).toHaveBeenCalledWith({ type: userActions.USER_LOGOUT });
- expect(settingsBackupSpy).toHaveBeenCalled();
- expect(resetSpy).toHaveBeenCalled();
- resetSpy.mockRestore();
- settingsBackupSpy.mockRestore();
- });
- });
-});
diff --git a/render/actions/helper.js b/render/actions/helper.js
index 47c9cefa..fe32a0b2 100644
--- a/render/actions/helper.js
+++ b/render/actions/helper.js
@@ -109,26 +109,6 @@ const request = ({
return axios.authorizedRequest(requestConfig);
};
-const login = ({
- redmineEndpoint,
- url,
- headers
-}) => axios.request({
- baseURL: redmineEndpoint,
- timeout: 20000,
- headers: headers || {},
- url,
- method: 'GET'
-}).then((res) => {
- const { api_key } = res.data.user || {};
- if (api_key) {
- axios.initialize(redmineEndpoint, api_key);
- }
- return { data: res.data };
-});
-
-const logout = () => axios.reset();
-
const notify = {
start: (type, info = {}) => ({ type, status: 'START', info }),
ok: (type, data, info = {}) => ({
@@ -143,8 +123,6 @@ const notify = {
export {
IssueFilter,
notify,
- login,
- logout
};
export default request;
diff --git a/render/actions/index.js b/render/actions/index.js
index a912f0b6..e5a7e954 100644
--- a/render/actions/index.js
+++ b/render/actions/index.js
@@ -1,4 +1,3 @@
-import userActions from './user.actions';
import trackingActions from './tracking.actions';
import issuesActions from './issues.actions';
import issueActions from './issue.actions';
@@ -7,7 +6,6 @@ import timeEntryActions from './timeEntry.actions';
import settingsActions from './settings.actions';
export default {
- user: userActions,
issues: issuesActions,
issue: issueActions,
tracking: trackingActions,
diff --git a/render/actions/user.actions.js b/render/actions/user.actions.js
deleted file mode 100644
index cadc74c1..00000000
--- a/render/actions/user.actions.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import settingsActions from './settings.actions';
-import request, { login, notify, logout } from './helper';
-
-export const USER_LOGIN = 'USER_LOGIN';
-export const USER_LOGOUT = 'USER_LOGOUT';
-export const USER_GET_CURRENT = 'USER_GET_CURRENT';
-
-const signout = () => (dispatch) => {
- logout();
- dispatch(settingsActions.backup());
- dispatch({ type: USER_LOGOUT });
-};
-
-const checkLogin = ({
- useApiKey, apiKey, username, password, redmineEndpoint
-}) => (dispatch) => {
- if (!redmineEndpoint) throw new Error('Unable to login to an undefined redmine endpoint');
-
- dispatch(notify.start(USER_LOGIN));
-
- const headers = {};
- if (useApiKey) {
- headers['X-Redmine-API-Key'] = apiKey;
- } else {
- headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
- }
-
- return login({
- redmineEndpoint,
- url: '/users/current.json',
- headers,
- }).then(({ data }) => {
- Object.assign(data.user, { redmineEndpoint });
- dispatch(notify.ok(USER_LOGIN, data));
- dispatch(settingsActions.restore());
- }).catch((error) => {
- // eslint-disable-next-line
- console.error('Error when trying to get the info about current user', error);
- dispatch(notify.nok(USER_LOGIN, error));
- });
-};
-
-const getCurrent = () => (dispatch) => {
- dispatch(notify.start(USER_GET_CURRENT));
-
- return request({
- url: '/users/current.json',
- id: 'getCurrentUserInfo'
- }).then(({ data }) => dispatch(notify.ok(USER_GET_CURRENT, data)))
- .catch((error) => {
- // eslint-disable-next-line
- console.error('Error when trying to get the info about current user', error);
- dispatch(notify.nok(USER_GET_CURRENT, error));
- });
-};
-
-export default {
- checkLogin,
- getCurrent,
- logout: signout
-};
diff --git a/render/components/IssueDetailsPage/TimeEntries.jsx b/render/components/IssueDetailsPage/TimeEntries.jsx
index c799f3c6..f130b54b 100644
--- a/render/components/IssueDetailsPage/TimeEntries.jsx
+++ b/render/components/IssueDetailsPage/TimeEntries.jsx
@@ -1,17 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+import { css as emotionCss } from '@emotion/react';
import styled, { css, withTheme } from 'styled-components';
import PlusIcon from 'mdi-react/PlusIcon';
-import CloseIcon from 'mdi-react/CloseIcon';
import TimerIcon from 'mdi-react/TimerIcon';
import InfiniteScroll from '../InfiniteScroll';
-import ProcessIndicator from '../ProcessIndicator';
-import Button, { GhostButton } from '../Button';
+import { ProcessIndicator } from '../ProcessIndicator';
+import Button from '../Button';
import DateComponent from '../Date';
-import Dialog from '../Dialog';
import actions from '../../actions';
const HeaderContainer = styled.div`
@@ -125,15 +124,20 @@ const ProcessIndicatorWrapper = styled.li`
position: absolute;
left: 24%;
bottom: 0;
-
- span {
- position: relative;
- bottom: 5px;
- left: 60px;
- }
}
`;
+const styles = {
+ processIndicatorText: emotionCss`
+ white-space: nowrap;
+ padding-left: 20px;
+ vertical-align: middle;
+ position: relative;
+ bottom: 5px;
+ left: 60px;
+ `
+};
+
class TimeEntries extends Component {
constructor(props) {
super(props);
@@ -206,7 +210,13 @@ class TimeEntries extends Component {
isEnd={spentTime.data.length === spentTime.totalCount}
// eslint-disable-next-line
hasMore={!spentTime.isFetching && !spentTime.error && spentTime.data.length < spentTime.totalCount}
- loadIndicator={}
+ loadIndicator={(
+
+
+ Please wait...
+
+
+ )}
container={this.listRef.current}
immediate={true}
>
diff --git a/render/components/IssueModal.jsx b/render/components/IssueModal.jsx
index 1839c004..2b26cdac 100644
--- a/render/components/IssueModal.jsx
+++ b/render/components/IssueModal.jsx
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import styled, { withTheme } from 'styled-components';
+import { css } from '@emotion/react';
import ClockIcon from 'mdi-react/ClockIcon';
import RawSlider from 'rc-slider';
@@ -9,7 +10,7 @@ import { Input, Label } from './Input';
import Button from './Button';
import ErrorMessage from './ErrorMessage';
import Modal from './Modal';
-import ProcessIndicator from './ProcessIndicator';
+import { ProcessIndicator } from './ProcessIndicator';
import Tooltip from './Tooltip';
import DatePicker from './DatePicker';
@@ -67,6 +68,14 @@ const DurationIcon = (
);
+const styles = {
+ processIndicatorText: css`
+ white-space: nowrap;
+ padding-left: 20px;
+ vertical-align: middle;
+ `
+};
+
class IssueModal extends Component {
constructor(props) {
super(props);
@@ -322,7 +331,7 @@ class IssueModal extends Component {
>
Submit
- { issue.isFetching && () }
+ { issue.isFetching && (Please wait...) }
>
diff --git a/render/components/LoadingOverlay.tsx b/render/components/LoadingOverlay.tsx
new file mode 100644
index 00000000..001a61a7
--- /dev/null
+++ b/render/components/LoadingOverlay.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { css } from '@emotion/react';
+import { ProcessIndicator } from './ProcessIndicator';
+
+const styles = {
+ wrapper: css`
+ display: flex;
+ height: 100%;
+ width: 100%;
+ justify-content: center;
+ `,
+ text: css`
+ margin-left: 1rem;
+ `
+};
+
+const LoadingOverlay = () => (
+
+);
+
+export {
+ LoadingOverlay
+};
diff --git a/render/components/Navbar.jsx b/render/components/Navbar.jsx
deleted file mode 100644
index c59ec357..00000000
--- a/render/components/Navbar.jsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { Component } from 'react';
-import styled, { css } from 'styled-components';
-import { connect } from 'react-redux';
-import { Link, NavLink } from 'react-router-dom';
-import PropTypes from 'prop-types';
-
-import actions from '../actions';
-import { GhostButton } from './Button';
-
-const Navbar = styled.nav`
- position: fixed;
- top: 0px;
- left: 0px;
- right: 0px;
- z-index: 2;
- height: 50px;
- background: linear-gradient(to bottom, ${(props) => props.theme.bg} 85%, transparent);
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20px 40px 0px 40px;
-
- ul {
- list-style-type: none;
- padding: 0;
- margin: 0;
- display: flex;
- align-items: center;
-
- li {
- display: inline;
- font-size: 15px;
- font-weight: bold;
- ${({ theme }) => css`
- color: ${theme.normalText};
- transition: color ease ${theme.transitionTime};
-
- a {
- text-decoration: none;
- color: ${theme.normalText};
- transition: color ease ${theme.transitionTime};
- padding-bottom: 5px;
- }
-
- a.active {
- color: ${theme.main};
- border-bottom: 2px solid ${theme.main};
- }
-
- &:hover {
- cursor: pointer;
- color: ${theme.main};
-
- a {
- color: ${theme.main};
- }
- }
- `}
- }
- }
-
- ul:first-child {
- li {
- margin: 0px 20px;
- }
-
- li:first-child {
- margin-left: 0;
- }
- }
-
- ul:last-child {
- li {
- margin: 0px 20px;
- }
-
- li:last-child {
- margin-right: 0px;
- }
- }
-`;
-
-class NavigationBar extends Component {
- signout = () => {
- const { logout } = this.props;
- logout();
- }
-
- render() {
- const { user = {} } = this.props;
- const { name } = user;
- return (
-
-
- -
-
- Summary
-
-
- {/* - Issues
*/}
-
-
- -
-
- {name}
-
-
- -
-
- Sign out
-
-
-
-
- );
- }
-}
-
-NavigationBar.propTypes = {
- user: PropTypes.shape({
- id: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number
- ]).isRequired,
- name: PropTypes.string.isRequired,
- api_key: PropTypes.string.isRequired,
- redmineEndpoint: PropTypes.string.isRequired
- }).isRequired,
- logout: PropTypes.func.isRequired
-};
-
-const mapStateToProps = (state) => ({
- user: state.user
-});
-
-const mapDispatchToProps = (dispatch) => ({
- logout: () => dispatch(actions.user.logout())
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(NavigationBar);
diff --git a/render/components/Navbar.tsx b/render/components/Navbar.tsx
new file mode 100644
index 00000000..6c4b89ed
--- /dev/null
+++ b/render/components/Navbar.tsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import styled, { css } from 'styled-components';
+import { Link, NavLink } from 'react-router-dom';
+
+import { GhostButton } from './Button';
+import { useOvermindActions, useOvermindState } from '../store';
+
+const StyledNavbar = styled.nav`
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ z-index: 2;
+ height: 50px;
+ background: linear-gradient(to bottom, ${props => props.theme.bg} 85%, transparent);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 20px 40px 0px 40px;
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ align-items: center;
+
+ li {
+ display: inline;
+ font-size: 15px;
+ font-weight: bold;
+ ${({ theme }) => css`
+ color: ${theme.normalText};
+ transition: color ease ${theme.transitionTime};
+
+ a {
+ text-decoration: none;
+ color: ${theme.normalText};
+ transition: color ease ${theme.transitionTime};
+ padding-bottom: 5px;
+ }
+
+ a.active {
+ color: ${theme.main};
+ border-bottom: 2px solid ${theme.main};
+ }
+
+ &:hover {
+ cursor: pointer;
+ color: ${theme.main};
+
+ a {
+ color: ${theme.main};
+ }
+ }
+ `}
+ }
+ }
+
+ ul:first-child {
+ li {
+ margin: 0px 20px;
+ }
+
+ li:first-child {
+ margin-left: 0;
+ }
+ }
+
+ ul:last-child {
+ li {
+ margin: 0px 20px;
+ }
+
+ li:last-child {
+ margin-right: 0px;
+ }
+ }
+`;
+
+const Navbar = () => {
+ const state = useOvermindState();
+ const actions = useOvermindActions();
+
+ const userName = `${state.users.currentUser?.firstName} ${state.users.currentUser?.lastName}`;
+
+ return (
+
+
+ -
+ Summary
+
+ {/* - Issues
*/}
+
+
+ -
+ {userName}
+
+ -
+
+ Sign out
+
+
+
+
+ );
+};
+
+export {
+ Navbar
+};
diff --git a/render/components/Notification.jsx b/render/components/Notification.jsx
index 6b5cff63..fabc87f0 100644
--- a/render/components/Notification.jsx
+++ b/render/components/Notification.jsx
@@ -2,8 +2,6 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
-import { report } from '../../common/reporter';
-
const NotifyButton = styled.button`
position: absolute;
width: 92%;
@@ -18,7 +16,7 @@ const NotifyButton = styled.button`
class Notification extends Component {
reportError = () => {
const { error } = this.props;
- report(error);
+ // report(error);
}
render() {
diff --git a/render/components/ProcessIndicator.jsx b/render/components/ProcessIndicator.tsx
similarity index 51%
rename from render/components/ProcessIndicator.jsx
rename to render/components/ProcessIndicator.tsx
index a2fa386c..a2c40c76 100644
--- a/render/components/ProcessIndicator.jsx
+++ b/render/components/ProcessIndicator.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import styled, { keyframes } from 'styled-components';
-import PropTypes from 'prop-types';
const flipAnimation = keyframes`
50% {
@@ -11,43 +10,31 @@ const flipAnimation = keyframes`
}
`;
-const ProcessIndicator = styled.div`
+const StyledProcessIndicator = styled.div`
display: flex;
align-items: center;
-
- span {
- white-space: nowrap;
- padding-left: 20px;
- vertical-align: middle;
- }
`;
const Square = styled.div`
- width: 2em;
- height: 2em;
background-color: ${(props) => props.theme.main};
transform: rotate(0);
animation: ${flipAnimation} 1s infinite;
`;
-const ProcessIndicatorComponent = ({ className }) => (
-
-
-
- Please Wait...
-
-
+type ProcessIndicatorProps = {
+ className?: string;
+ size?: string;
+ children: JSX.Element;
+}
+
+const ProcessIndicator = ({ className, size = '2em', children }: ProcessIndicatorProps) => (
+
+
+ {children}
+
);
-ProcessIndicatorComponent.propTypes = {
- className: PropTypes.string
-};
-
-ProcessIndicatorComponent.defaultProps = {
- className: null
-};
-
-export const OverlayProcessIndicator = styled(ProcessIndicatorComponent)`
+const OverlayProcessIndicator = styled(ProcessIndicator)`
justify-content: center;
position: absolute;
top: 45%;
@@ -59,4 +46,7 @@ export const OverlayProcessIndicator = styled(ProcessIndicatorComponent)`
border-radius: 3px;
`;
-export default ProcessIndicatorComponent;
+export {
+ ProcessIndicator,
+ OverlayProcessIndicator
+};
diff --git a/render/components/SummaryPage/IssuesTable.jsx b/render/components/SummaryPage/IssuesTable.jsx
index 6fabbd9b..7e42bb30 100644
--- a/render/components/SummaryPage/IssuesTable.jsx
+++ b/render/components/SummaryPage/IssuesTable.jsx
@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _get from 'lodash/get';
+import { css as emotionCss } from '@emotion/react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import styled, { withTheme, css } from 'styled-components';
@@ -8,7 +9,7 @@ import SortAscendingIcon from 'mdi-react/SortAscendingIcon';
import SortDescendingIcon from 'mdi-react/SortDescendingIcon';
import InfiniteScroll from '../InfiniteScroll';
-import ProcessIndicator, { OverlayProcessIndicator } from '../ProcessIndicator';
+import { ProcessIndicator, OverlayProcessIndicator } from '../ProcessIndicator';
import Date from '../Date';
const Table = styled.table`
@@ -89,6 +90,14 @@ const ColorfulSpan = styled.span`
}
`;
+const styles = {
+ processIndicatorText: emotionCss`
+ white-space: nowrap;
+ padding-left: 20px;
+ vertical-align: middle;
+ `
+};
+
const colorMap = {
closed: 'red',
high: 'yellow',
@@ -157,7 +166,7 @@ class IssuesTable extends Component {
const userTasks = issues.data;
return (
<>
- { (!userTasks.length && issues.isFetching) && () }
+ { (!userTasks.length && issues.isFetching) && (Please wait...) }
@@ -187,7 +196,9 @@ class IssuesTable extends Component {
loadIndicator={(
|
-
+
+ Please wait...
+
|
)}
diff --git a/render/components/TimeEntryModal.jsx b/render/components/TimeEntryModal.jsx
index 7bbb394a..156ebec3 100644
--- a/render/components/TimeEntryModal.jsx
+++ b/render/components/TimeEntryModal.jsx
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Select from 'react-select';
import styled, { withTheme } from 'styled-components';
+import { css } from '@emotion/react';
import ClockIcon from 'mdi-react/ClockIcon';
import { Input, Label } from './Input';
@@ -13,7 +14,7 @@ import MarkdownEditor from './MarkdownEditor';
import ErrorMessage from './ErrorMessage';
import DatePicker from './DatePicker';
import Modal from './Modal';
-import ProcessIndicator from './ProcessIndicator';
+import { ProcessIndicator } from './ProcessIndicator';
import Tooltip from './Tooltip';
import actions from '../actions';
@@ -65,6 +66,14 @@ const DurationIcon = (
);
+const styles = {
+ processIndicatorText: css`
+ white-space: nowrap;
+ padding-left: 20px;
+ vertical-align: middle;
+ `
+};
+
const selectStyles = {
container: (base) => ({ ...base })
};
@@ -364,7 +373,7 @@ class TimeEntryModal extends Component {
>
Submit
- { time.isFetching && () }
+ { time.isFetching && (Please wait...) }
)
: (
@@ -377,7 +386,7 @@ class TimeEntryModal extends Component {
>
Submit
- { time.isFetching && () }
+ { time.isFetching && (Please wait...) }
)}
>
diff --git a/render/components/__tests__/Navbar.spec.jsx b/render/components/__tests__/Navbar.spec.jsx
index 07280ef9..c8e7529d 100644
--- a/render/components/__tests__/Navbar.spec.jsx
+++ b/render/components/__tests__/Navbar.spec.jsx
@@ -6,8 +6,7 @@ import thunk from 'redux-thunk';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
-import { USER_LOGOUT } from '../../actions/user.actions';
-import Navbar from '../Navbar';
+import { Navbar } from '../Navbar';
const mockStore = configureStore([thunk]);
@@ -58,8 +57,5 @@ describe('Navbar component', () => {
expect(signoutBtn).toBeDefined();
fireEvent.click(signoutBtn);
expect(store.getActions().length).toBe(2);
- expect(store.getActions().pop()).toEqual({
- type: USER_LOGOUT
- });
});
});
diff --git a/render/helpers/utils.ts b/render/helpers/utils.ts
new file mode 100644
index 00000000..747d68df
--- /dev/null
+++ b/render/helpers/utils.ts
@@ -0,0 +1,5 @@
+const getStoredToken = () => localStorage.getItem('token');
+
+export {
+ getStoredToken
+};
diff --git a/render/reducers/__tests__/project.reducer.spec.js b/render/reducers/__tests__/project.reducer.spec.js
index 00abd067..479d4d2e 100644
--- a/render/reducers/__tests__/project.reducer.spec.js
+++ b/render/reducers/__tests__/project.reducer.spec.js
@@ -3,7 +3,6 @@ import _cloneDeep from 'lodash/cloneDeep';
import reducer, { initialState } from '../project.reducer';
import { notify } from '../../actions/helper';
import * as actions from '../../actions/project.actions';
-import storage from '../../../common/storage';
describe('Project reducer', () => {
it('should return the initial state if an unknown action comes in', () => {
@@ -29,7 +28,6 @@ describe('Project reducer', () => {
});
it('status OK', () => {
- const storageSpy = jest.spyOn(storage, 'set');
const error = new Error('Whoops');
const data = [
{
@@ -128,8 +126,6 @@ describe('Project reducer', () => {
notify.ok(actions.PROJECT_GET_ALL, data)
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith('projects', expectedNextState);
- storageSpy.mockRestore();
});
it('status NOK', () => {
diff --git a/render/reducers/__tests__/settings.reducer.spec.js b/render/reducers/__tests__/settings.reducer.spec.js
index fa1c2573..338bf788 100644
--- a/render/reducers/__tests__/settings.reducer.spec.js
+++ b/render/reducers/__tests__/settings.reducer.spec.js
@@ -1,24 +1,9 @@
import _cloneDeep from 'lodash/cloneDeep';
import reducer, { initialState } from '../settings.reducer';
-import storage from '../../../common/storage';
import * as actions from '../../actions/settings.actions';
-let storageSpy;
-
describe('Settings reducer', () => {
- beforeAll(() => {
- storageSpy = jest.spyOn(storage, 'set');
- });
-
- afterEach(() => {
- storageSpy.mockReset();
- });
-
- afterAll(() => {
- storageSpy.mockRestore();
- });
-
it('should return the initial state by default', () => {
expect(reducer(undefined, { type: 'NONE' })).toEqual(initialState);
});
@@ -43,10 +28,6 @@ describe('Settings reducer', () => {
}
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith(
- `settings.${data.redmineEndpoint}.${data.userId}`,
- expectedNextState
- );
});
it('should handle SETTINGS_USE_COLORS action', () => {
@@ -69,10 +50,6 @@ describe('Settings reducer', () => {
}
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith(
- `settings.${data.redmineEndpoint}.${data.userId}`,
- expectedNextState
- );
});
it('should handle SETTINGS_ISSUE_HEADERS action', () => {
@@ -101,10 +78,6 @@ describe('Settings reducer', () => {
}
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith(
- `settings.${data.redmineEndpoint}.${data.userId}`,
- expectedNextState
- );
});
it('should handle SETTINGS_BACKUP action', () => {
@@ -121,7 +94,6 @@ describe('Settings reducer', () => {
}
)
).toEqual(initialState);
- expect(storageSpy).toHaveBeenCalledWith(`settings.${data.redmineEndpoint}.${data.userId}`, initialState);
});
it('should handle SETTINGS_RESTORE action', () => {
@@ -129,7 +101,7 @@ describe('Settings reducer', () => {
redmineEndpoint: 'https://redmine.redmine',
userId: 1
};
- const storageGetSpy = jest.spyOn(storage, 'get').mockReturnValue(_cloneDeep(initialState));
+
expect(
reducer(
_cloneDeep(initialState),
@@ -139,6 +111,5 @@ describe('Settings reducer', () => {
}
)
).toEqual(initialState);
- expect(storageGetSpy).toHaveBeenCalledWith(`settings.${data.redmineEndpoint}.${data.userId}`, initialState);
});
});
diff --git a/render/reducers/__tests__/tracking.reducer.spec.js b/render/reducers/__tests__/tracking.reducer.spec.js
index b7ea2aca..3750cdda 100644
--- a/render/reducers/__tests__/tracking.reducer.spec.js
+++ b/render/reducers/__tests__/tracking.reducer.spec.js
@@ -1,24 +1,9 @@
import _cloneDeep from 'lodash/cloneDeep';
-import storage from '../../../common/storage';
import reducer, { initialState } from '../tracking.reducer';
import actions from '../../actions/tracking.actions';
-let storageSpy;
-
describe('Tracking Reducer', () => {
- beforeAll(() => {
- storageSpy = jest.spyOn(storage, 'set');
- });
-
- afterEach(() => {
- storageSpy.mockReset();
- });
-
- afterAll(() => {
- storageSpy.mockRestore();
- });
-
it('should return the initial state if an unknown action comes in', () => {
expect(
reducer(
@@ -43,7 +28,6 @@ describe('Tracking Reducer', () => {
actions.trackingStart({ id: 1 })
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith('time_tracking', expectedNextState);
});
it('TRACKING_STOP action', () => {
@@ -61,7 +45,6 @@ describe('Tracking Reducer', () => {
actions.trackingStop(1000)
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith('time_tracking', expectedNextState);
});
it('TRACKING_PAUSE action', () => {
@@ -82,7 +65,6 @@ describe('Tracking Reducer', () => {
actions.trackingPause(1000)
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith('time_tracking', expectedNextState);
});
it('TRACKING_CONTINUE action', () => {
@@ -104,17 +86,14 @@ describe('Tracking Reducer', () => {
actions.trackingContinue()
)
).toEqual(expectedNextState);
- expect(storageSpy).toHaveBeenCalledWith('time_tracking', expectedNextState);
});
it('TRACKING_RESET action', () => {
- const storageDeleteSpy = jest.spyOn(storage, 'delete');
expect(
reducer(
_cloneDeep(initialState),
actions.trackingReset()
)
).toEqual(initialState);
- expect(storageDeleteSpy).toHaveBeenCalledWith('time_tracking');
});
});
diff --git a/render/reducers/__tests__/user.reducer.spec.js b/render/reducers/__tests__/user.reducer.spec.js
deleted file mode 100644
index 780731d0..00000000
--- a/render/reducers/__tests__/user.reducer.spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import reducer from '../user.reducer';
-import { USER_LOGIN, USER_LOGOUT } from '../../actions/user.actions';
-import { notify } from '../../actions/helper';
-
-import storage from '../../../common/storage';
-
-jest.mock('electron-store');
-
-describe('User reducer', () => {
- const initialState = {
- isFetching: false,
- loginError: undefined,
- id: undefined,
- firstname: undefined,
- lastname: undefined,
- redmineEndpoint: undefined,
- api_key: undefined
- };
-
- it('should return the initial state by default', () => {
- expect(reducer(undefined, { type: 'NONE' })).toEqual(initialState);
- });
-
- describe('USER_LOGIN action', () => {
- afterEach(storage.clear);
-
- it('should set isFetching to true during status START', () => {
- expect(reducer(initialState, notify.start(USER_LOGIN))).toEqual({
- ...initialState,
- isFetching: true
- });
- });
-
- it('should get user data and put it in storage on OK', () => {
- const storageSetSpy = jest.spyOn(storage, 'set');
- const data = {
- user: {
- id: 1,
- firstname: 'firstname',
- lastname: 'lastname',
- redmineEndpoint: 'https://redmine.domain',
- api_key: '123abc'
- }
- };
- expect(reducer(initialState, notify.ok(USER_LOGIN, data))).toEqual({
- ...initialState,
- id: data.user.id,
- name: `${data.user.firstname} ${data.user.lastname}`,
- redmineEndpoint: data.user.redmineEndpoint,
- api_key: data.user.api_key
- });
- expect(storageSetSpy).toHaveBeenCalledWith('user', {
- id: data.user.id,
- name: `${data.user.firstname} ${data.user.lastname}`,
- redmineEndpoint: data.user.redmineEndpoint,
- api_key: data.user.api_key
- });
-
- storageSetSpy.mockRestore();
- });
-
- it('should set error on NOK', () => {
- const error = new Error('ERROR');
- expect(reducer(initialState, notify.nok(USER_LOGIN, error))).toEqual({
- ...initialState,
- loginError: error
- });
- });
- });
-
- describe('USER_LOGOUT action', () => {
- it('should wipe the storage leaving only settings', () => {
- const storageGetSpy = jest.spyOn(storage, 'get').mockImplementation(() => ({
- cors: true,
- theme: 'dark'
- }));
- const storageSetSpy = jest.spyOn(storage, 'set');
- const storageClearSpy = jest.spyOn(storage, 'clear');
-
- const defaultState = {
- isFetching: false,
- loginError: undefined,
- id: 1,
- firstname: 'firstname',
- lastname: 'lastname',
- redmineEndpoint: 'https://redmine.domain',
- api_key: '123abc'
- };
- expect(
- reducer(
- defaultState,
- {
- type: USER_LOGOUT
- }
- )
- );
-
- expect(storageGetSpy).toHaveBeenCalledWith('settings');
- expect(storageClearSpy).toHaveBeenCalled();
- expect(storageSetSpy).toHaveBeenCalledWith('settings', {
- cors: true,
- theme: 'dark'
- });
-
- storageGetSpy.mockRestore();
- storageClearSpy.mockRestore();
- storageSetSpy.mockRestore();
- });
- });
-});
diff --git a/render/reducers/index.js b/render/reducers/index.js
index deff2168..cb270ce1 100644
--- a/render/reducers/index.js
+++ b/render/reducers/index.js
@@ -1,5 +1,4 @@
import { combineReducers } from 'redux';
-import usersReducer from './user.reducer';
import settingsReducer from './settings.reducer';
import allIssuesReducer from './issues.reducer';
import issueReducer from './issue.reducer';
@@ -8,10 +7,7 @@ import trackingReducer from './tracking.reducer';
import projectReducer from './project.reducer';
import timeEntryReducer from './timeEntry.reducer';
-import { USER_LOGOUT } from '../actions/user.actions';
-
const appReducer = combineReducers({
- user: usersReducer,
settings: settingsReducer,
issues: combineReducers({
all: allIssuesReducer,
@@ -25,9 +21,6 @@ const appReducer = combineReducers({
export default (state, action) => {
switch (action.type) {
- case USER_LOGOUT: {
- return appReducer(undefined, action);
- }
default:
return appReducer(state, action);
}
diff --git a/render/reducers/project.reducer.js b/render/reducers/project.reducer.js
index 9b5ae83b..799ffca8 100644
--- a/render/reducers/project.reducer.js
+++ b/render/reducers/project.reducer.js
@@ -1,6 +1,5 @@
import _ from 'lodash';
-// import storage from '../../common/storage';
import { PROJECT_GET_ALL } from '../actions/project.actions';
export const initialState = {
diff --git a/render/reducers/tracking.reducer.js b/render/reducers/tracking.reducer.js
index 079a922b..711040eb 100644
--- a/render/reducers/tracking.reducer.js
+++ b/render/reducers/tracking.reducer.js
@@ -6,7 +6,6 @@ import {
TRACKING_SAVE,
TRACKING_RESET
} from '../actions/tracking.actions';
-// import storage from '../../common/storage';
export const initialState = {
issue: {},
diff --git a/render/reducers/user.reducer.js b/render/reducers/user.reducer.js
deleted file mode 100644
index a167e687..00000000
--- a/render/reducers/user.reducer.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import _ from 'lodash';
-import { USER_LOGIN, USER_LOGOUT } from '../actions/user.actions';
-// import storage from '../../common/storage';
-
-export const initialState = {
- isFetching: false,
- loginError: undefined,
- id: undefined,
- name: undefined,
- redmineEndpoint: undefined,
- api_key: undefined
-};
-
-const handleUserLogin = (state, action) => {
- switch (action.status) {
- case 'START': {
- return { ...initialState, isFetching: true };
- }
- case 'OK': {
- const userData = _.get(action.data, 'user', {});
- const { firstname, lastname } = userData;
- const payload = _.pick(userData, 'id', 'redmineEndpoint', 'api_key');
- payload.name = `${firstname} ${lastname}`;
- // storage.user.set(payload);
- return {
- ...state, ...payload, isFetching: false, loginError: undefined
- };
- }
- case 'NOK': {
- return { ...state, loginError: action.data, isFetching: false };
- }
- default:
- return state;
- }
-};
-
-export default (state = initialState, action) => {
- switch (action.type) {
- case USER_LOGIN: {
- return handleUserLogin(state, action);
- }
- case USER_LOGOUT: {
- // we keep settings cause they are general app settings
- // const settings = storage.settings.get('settings');
- // storage.clear();
- // if (settings) {
- // storage.set('settings', settings);
- // }
- return { ...initialState };
- }
- default:
- return state;
- }
-};
diff --git a/render/reduxStore.ts b/render/reduxStore.ts
index 3335509e..c3e18e20 100644
--- a/render/reduxStore.ts
+++ b/render/reduxStore.ts
@@ -2,12 +2,9 @@ import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
// import _get from 'lodash/get';
-// import storage from '../common/storage';
import reducers from './reducers/index';
import notificationMiddleware from './middlewares/notification.middleware';
-// const initialState = storage.store;
-
// const user = _get(initialState, 'user', {});
// const { id, redmineEndpoint } = user;
// const userSettings = _get(initialState, `settings.${redmineEndpoint}.${id}`);
diff --git a/render/store/effects/index.ts b/render/store/effects/index.ts
index 07be1e6c..49edb5bd 100644
--- a/render/store/effects/index.ts
+++ b/render/store/effects/index.ts
@@ -1,7 +1,5 @@
-import * as request from './request';
-import * as storage from './storage';
+import * as mainProcess from './mainProcess';
export {
- request,
- storage
+ mainProcess
};
diff --git a/render/store/effects/mainProcess.ts b/render/store/effects/mainProcess.ts
new file mode 100644
index 00000000..d101e469
--- /dev/null
+++ b/render/store/effects/mainProcess.ts
@@ -0,0 +1,37 @@
+import { ipcRenderer, remote } from 'electron';
+import { Response } from '../../../types';
+
+const crypto = remote.require('crypto');
+
+type MainProcessEventData = {
+ payload: Record;
+ action: string;
+}
+
+type MainProcessEventTags = {
+ reqEvent: string;
+ resEvent: string;
+}
+
+const query = ({ reqEvent, resEvent }: MainProcessEventTags, payload: MainProcessEventData): Promise => {
+ const id = crypto.randomBytes(10).toString('hex');
+
+ return new Promise((resolve) => {
+ ipcRenderer.send(reqEvent, JSON.stringify({ ...payload, id }));
+
+ ipcRenderer.once(`${resEvent}:${id}`, (event, response) => {
+ console.log(resEvent, response);
+ resolve(response);
+ });
+ });
+};
+
+const request = (payload: MainProcessEventData) => query({ reqEvent: 'request', resEvent: 'response' }, payload);
+const system = (payload: MainProcessEventData) => query({ reqEvent: 'system-request', resEvent: 'system-response' }, payload);
+const session = (payload: MainProcessEventData) => query({ reqEvent: 'session-request', resEvent: 'session-response' }, payload);
+
+export {
+ request,
+ system,
+ session
+};
diff --git a/render/store/effects/request.ts b/render/store/effects/request.ts
deleted file mode 100644
index bcae933d..00000000
--- a/render/store/effects/request.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ipcRenderer, remote } from 'electron';
-import { Response } from '../../../types';
-
-const crypto = remote.require('crypto');
-
-type Config = {
- endpoint: string;
- token?: string;
-}
-
-type QueryConfig = {
- payload: Record;
- config?: Config;
-}
-
-const query = (config: QueryConfig): Promise => {
- const id = crypto.randomBytes(10).toString('hex');
-
- return new Promise((resolve) => {
- ipcRenderer.send('request', JSON.stringify({ ...config, id }));
-
- ipcRenderer.once(`response:${id}`, (event, response) => {
- console.log('response:', response);
- resolve(response);
- });
- });
-};
-
-export {
- query,
-};
-
-export type {
- QueryConfig
-};
diff --git a/render/store/effects/storage.ts b/render/store/effects/storage.ts
deleted file mode 100644
index 343ae509..00000000
--- a/render/store/effects/storage.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ipcRenderer, remote } from 'electron';
-import type { Context } from 'overmind';
-import { StorageAction } from '../../../types';
-
-const crypto = remote.require('crypto');
-
-const save = (data: Context['state']['settings']) => {
- const id = crypto.randomBytes(10).toString('hex');
-
- return new Promise((resolve) => {
- ipcRenderer.send('storage', JSON.stringify({ action: StorageAction.SAVE, payload: data, id }));
-
- ipcRenderer.once(`storage:${id}`, (event, response) => {
- console.log('storage:save', response);
- resolve(response);
- });
- });
-};
-
-type ReadArgs = {
- userId: string;
- endpoint: string;
-};
-
-const read = (payload: ReadArgs): Promise => {
- const id = crypto.randomBytes(10).toString('hex');
-
- return new Promise((resolve) => {
- ipcRenderer.send('storage', JSON.stringify({ action: StorageAction.READ, payload, id }));
-
- ipcRenderer.once(`storage:${id}`, (event, response) => {
- console.log('storage:read', response);
- resolve(response);
- });
- });
-};
-
-export {
- save,
- read
-};
diff --git a/render/store/index.ts b/render/store/index.ts
index 38c6ea64..3f93a886 100644
--- a/render/store/index.ts
+++ b/render/store/index.ts
@@ -7,15 +7,14 @@ import { merge, namespaced } from 'overmind/config';
import * as settings from './settings';
import * as users from './users';
-import { request, storage } from './effects';
+import { mainProcess } from './effects';
const overmindStoreConfig = merge(
{
state: {},
actions: {},
effects: {
- request,
- storage
+ mainProcess,
}
},
namespaced({
diff --git a/render/store/settings/actions.ts b/render/store/settings/actions.ts
index 6e233ac6..f8f8c340 100644
--- a/render/store/settings/actions.ts
+++ b/render/store/settings/actions.ts
@@ -1,22 +1,104 @@
import type { IAction, Context } from 'overmind';
-import type { State } from './state';
+import { defaultSettingsState, SettingsState } from './state';
-const update: IAction = ({ state, effects }: Context, settings) => {
+import { Response, SessionAction, User } from '../../../types';
+import { getStoredToken } from '../../helpers/utils';
+
+const update: IAction> = ({ state, effects }: Context, settings) => {
state.settings = {
...settings
};
- effects.storage.save(state.settings);
+
+ const { endpoint, ...appSettings } = settings;
+ const currentUser = state.users.currentUser as User;
+
+ return effects.mainProcess.session({
+ action: SessionAction.SAVE,
+ payload: {
+ user: {
+ id: currentUser.id,
+ firstName: currentUser.firstName,
+ lastName: currentUser.lastName,
+ createdOn: currentUser.createdOn
+ },
+ endpoint,
+ token: getStoredToken(),
+ settings: {
+ ...appSettings
+ }
+ }
+ });
};
-const restore: IAction> = async ({ state, effects }: Context) => {
- const settings = await effects.storage.read({
- userId: state.users.currentUser?.id as string,
- endpoint: state.settings.endpoint as string
+const restore: IAction> = async ({ state, effects }: Context, token) => {
+ const response = await effects.mainProcess.session({
+ action: SessionAction.READ,
+ payload: {
+ token
+ }
+ });
+
+ if (response.success && response.payload) {
+ const { endpoint, settings } = response.payload;
+ const currentUser = response.payload.user as User;
+
+ // replacing the active session
+ const saveResponse = await effects.mainProcess.session({
+ action: SessionAction.SAVE,
+ payload: {
+ user: {
+ id: currentUser.id,
+ firstName: currentUser.firstName,
+ lastName: currentUser.lastName,
+ createdOn: currentUser.createdOn
+ },
+ endpoint,
+ token: getStoredToken(),
+ settings
+ }
+ });
+
+ if (!saveResponse.success) {
+ return { success: false };
+ }
+
+ state.users.currentUser = { ...currentUser };
+
+ state.settings = {
+ endpoint: response.payload.endpoint,
+ ...settings
+ };
+ }
+
+ return { success: response.success };
+};
+
+const reset: IAction> = async ({ effects, state }: Context) => {
+ state.settings = { ...defaultSettingsState };
+
+ const { endpoint, ...appSettings } = defaultSettingsState;
+ const currentUser = state.users.currentUser as User;
+
+ return effects.mainProcess.session({
+ action: SessionAction.SAVE,
+ payload: {
+ user: {
+ id: currentUser?.id,
+ firstName: currentUser?.firstName,
+ lastName: currentUser?.lastName,
+ createdOn: currentUser?.createdOn
+ },
+ endpoint,
+ token: getStoredToken(),
+ settings: {
+ ...appSettings
+ }
+ }
});
- state.settings = { ...settings };
};
export {
update,
- restore
+ restore,
+ reset
};
diff --git a/render/store/settings/state.ts b/render/store/settings/state.ts
index 6fcf5289..d98fd8c0 100644
--- a/render/store/settings/state.ts
+++ b/render/store/settings/state.ts
@@ -1,13 +1,12 @@
import type { IssueHeader } from '../../../types';
-type State = {
+type SettingsState = {
showClosedIssues: boolean;
issueHeaders: IssueHeader[];
- apiKey?: string;
endpoint?: string;
}
-const state: State = {
+const defaultSettingsState = {
showClosedIssues: false,
issueHeaders: [
{ label: 'Id', isFixed: true, value: 'id' },
@@ -19,14 +18,18 @@ const state: State = {
{ label: 'Estimation', isFixed: false, value: 'estimated_hours' },
{ label: 'Due Date', isFixed: false, value: 'due_date' }
],
- apiKey: undefined,
endpoint: undefined
};
+const state: SettingsState = {
+ ...defaultSettingsState
+};
+
export {
state,
+ defaultSettingsState
};
export type {
- State
+ SettingsState
};
diff --git a/render/store/users/actions.ts b/render/store/users/actions.ts
index 5c0425d6..5e58eebe 100644
--- a/render/store/users/actions.ts
+++ b/render/store/users/actions.ts
@@ -1,5 +1,7 @@
import type { IAction, Context } from 'overmind';
import { Response } from '../../../types';
+import { getStoredToken } from '../../helpers/utils';
+import { defaultSettingsState } from '../settings/state';
type LoginActionProps = {
useApiKey: boolean;
@@ -9,39 +11,54 @@ type LoginActionProps = {
redmineEndpoint?: string;
};
-const login: IAction> = async ({ effects, state }: Context, {
+const login: IAction> = async ({ actions, effects, state }: Context, {
apiKey, username, password, redmineEndpoint
}) => {
if (!redmineEndpoint) {
throw new Error('Unable to send a request to an undefined redmine endpoint');
}
- const headers: Record = apiKey ? {
- 'X-Redmine-API-Key': apiKey
+ const token = apiKey || getStoredToken() || undefined;
+
+ const headers: Record = token ? {
+ 'X-Redmine-API-Key': token
} : {
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
};
- const response = await effects.request.query({
+ const loginResponse = await effects.mainProcess.system({
+ action: 'login',
payload: {
headers,
- route: 'users/current.json',
- },
- config: {
endpoint: redmineEndpoint,
- token: apiKey
+ token
}
});
- if (response.success) {
- state.users.currentUser = response.data;
+ if (loginResponse.success) {
+ state.users.currentUser = loginResponse.payload;
+ localStorage.setItem('token', loginResponse.payload.token);
+
+ const restoreResponse = await actions.settings.restore(loginResponse.payload.token);
+
+ if (!restoreResponse.success) {
+ await actions.settings.update({ ...defaultSettingsState, endpoint: redmineEndpoint });
+ }
}
- return response;
+ return loginResponse;
};
-const logout = () => {
- /* noop */
+const logout: IAction> = async ({ state, effects }: Context) => {
+ const response = await effects.mainProcess.system({
+ action: 'logout',
+ payload: {}
+ });
+
+ if (response.success) {
+ localStorage.removeItem('token');
+ state.users.currentUser = undefined;
+ }
};
export {
diff --git a/render/store/users/state.ts b/render/store/users/state.ts
index 04730d87..33837090 100644
--- a/render/store/users/state.ts
+++ b/render/store/users/state.ts
@@ -1,14 +1,10 @@
-type State = {
- currentUser?: {
- id: string;
- firstName: string;
- lastName: string;
- createdOn: Date;
- lastLoggedOn: Date;
- }
+import type { User } from '../../../types';
+
+type UserState = {
+ currentUser?: User
}
-const state: State = {
+const state: UserState = {
currentUser: undefined
};
@@ -17,5 +13,5 @@ export {
};
export type {
- State
+ UserState
};
diff --git a/render/views/AppView.tsx b/render/views/AppView.tsx
index 327a7eb7..97f6a365 100644
--- a/render/views/AppView.tsx
+++ b/render/views/AppView.tsx
@@ -9,13 +9,12 @@ import moment from 'moment';
import { ipcRenderer } from 'electron';
import actions from '../actions';
-import Navbar from '../components/Navbar';
+import { Navbar } from '../components/Navbar';
import Timer from '../components/Timer';
import SummaryPage from './AppViewPages/SummaryPage';
import IssueDetailsPage from './AppViewPages/IssueDetailsPage';
import TimeEntryModal from '../components/TimeEntryModal';
import DragArea from '../components/DragArea';
-// import storage from '../../common/storage';
import { hoursToDuration } from '../datetime';
import { useOvermindState } from '../store';
@@ -77,7 +76,6 @@ const AppView = ({
name: `${state.users.currentUser?.firstName} ${state.users.currentUser?.lastName}`
}
});
- // storage.delete('time_tracking');
};
const closeTimeEntryModal = () => {
@@ -92,7 +90,7 @@ const AppView = ({
{!state.users.currentUser && ()}
- } />
+ } />
} />
({
- api_key: state.user.api_key,
projects: state.projects.data,
idleBehavior: state.settings.idleBehavior,
discardIdleTime: state.settings.discardIdleTime,
@@ -143,7 +138,6 @@ const mapStateToProps = (state: any) => ({
});
const mapDispatchToProps = (dispatch: any) => ({
- logout: () => dispatch(actions.user.logout()),
getProjectData: () => dispatch(actions.projects.getAll()),
resetTimer: () => dispatch(actions.tracking.trackingReset())
});
diff --git a/render/views/AppViewPages/IssueDetailsPage.tsx b/render/views/AppViewPages/IssueDetailsPage.tsx
index 2a35a184..acf4e0a8 100644
--- a/render/views/AppViewPages/IssueDetailsPage.tsx
+++ b/render/views/AppViewPages/IssueDetailsPage.tsx
@@ -421,7 +421,11 @@ const IssueDetailsPage = ({
)}
)
- : ;
+ : (
+
+ Please wait...
+
+ );
};
IssueDetailsPage.propTypes = {
diff --git a/render/views/LoginView.tsx b/render/views/LoginView.tsx
index 3724c330..70128128 100644
--- a/render/views/LoginView.tsx
+++ b/render/views/LoginView.tsx
@@ -107,7 +107,7 @@ const LoginView = () => {
if (error) {
setLoginError(error);
} else if (success) {
- history.push('/app/summary');
+ history.push('/app');
}
});
};
diff --git a/render/views/__tests__/AppView.spec.jsx b/render/views/__tests__/AppView.spec.jsx
index 30a9ec6a..31db0487 100644
--- a/render/views/__tests__/AppView.spec.jsx
+++ b/render/views/__tests__/AppView.spec.jsx
@@ -10,13 +10,11 @@ import '@testing-library/jest-dom/extend-expect';
import configureStore from 'redux-mock-store';
import { ThemeProvider } from 'styled-components';
-import { USER_LOGOUT } from '../../actions/user.actions';
import { PROJECT_GET_ALL } from '../../actions/project.actions';
import { TRACKING_RESET } from '../../actions/tracking.actions';
import * as axios from '../../../common/request';
import theme from '../../theme';
import AppView from '../AppView';
-import storage from '../../../common/storage';
jest.mock('electron-store');
@@ -209,7 +207,6 @@ describe('AppView', () => {
expect(historyMock.push).toHaveBeenCalledWith('/');
const actions = store.getActions();
expect(actions.length).toBeGreaterThan(0);
- expect(actions.pop()).toEqual({ type: USER_LOGOUT });
});
it('should open the modal with a new timeEntry if timer stops and wipe the storage', (done) => {
@@ -279,8 +276,6 @@ describe('AppView', () => {
}
};
- const storageSpy = jest.spyOn(storage, 'delete');
-
render(
@@ -299,13 +294,11 @@ describe('AppView', () => {
);
fireEvent.click(document.querySelector('#stop-timer'));
- expect(storageSpy).toHaveBeenCalledWith('time_tracking');
fireEvent.click(document.querySelector('#btn-add'));
setTimeout(() => {
expect(
store.getActions().find((action) => action.type === TRACKING_RESET)
).toBeDefined();
- storageSpy.mockRestore();
done();
}, 1);
});
diff --git a/render/views/__tests__/LoginView.spec.jsx b/render/views/__tests__/LoginView.spec.jsx
index d97dbcf7..ee876510 100644
--- a/render/views/__tests__/LoginView.spec.jsx
+++ b/render/views/__tests__/LoginView.spec.jsx
@@ -11,9 +11,7 @@ import { ThemeProvider } from 'styled-components';
import MockAdapter from 'axios-mock-adapter';
import actions from '../../actions';
-import { USER_LOGIN } from '../../actions/user.actions';
import { notify } from '../../actions/helper';
-import storage from '../../../common/storage';
import axios from '../../../common/request';
import theme from '../../theme';
@@ -31,7 +29,6 @@ describe('Login view', () => {
afterEach(() => {
cleanup();
axiosMock.reset();
- storage.clear();
});
afterAll(() => {
@@ -75,7 +72,6 @@ describe('Login view', () => {
it('should not make a redmine api request if the form has errors', (done) => {
const store = mockStore({ user: {} });
- const storageSetSpy = jest.spyOn(storage, 'set');
axiosMock.onGet('/users/current.json').reply(() => Promise.resolve([200]));
const { getAllByText } = render(
@@ -93,9 +89,7 @@ describe('Login view', () => {
fireEvent.submit(submitButton);
setTimeout(() => {
expect(getAllByText(/is not allowed to be empty$/).length).toBeGreaterThan(0);
- expect(storageSetSpy).not.toHaveBeenCalled();
expect(store.getActions()).toHaveLength(0);
- storageSetSpy.mockRestore();
done();
}, 100);
});
@@ -168,13 +162,6 @@ describe('Login view', () => {
.toBe(`Basic ${btoa(`${returnedValues.username}:${returnedValues.password}`)}`);
expect(store.getActions()).toHaveLength(3);
- expect(store.getActions()[0]).toEqual(notify.start(USER_LOGIN));
- expect(store.getActions()[1]).toEqual(notify.ok(USER_LOGIN, {
- user: {
- ...userData,
- redmineEndpoint: returnedValues.redmineEndpoint
- }
- }));
store.clearActions();
done();
@@ -244,13 +231,6 @@ describe('Login view', () => {
expect(axiosMock.history.get[0].url).toBe('/users/current.json');
expect(axiosMock.history.get[0].headers['X-Redmine-API-Key']).toBe(returnedValues.apiKey);
expect(store.getActions()).toHaveLength(3);
- expect(store.getActions()[0]).toEqual(notify.start(USER_LOGIN));
- expect(store.getActions()[1]).toEqual(notify.ok(USER_LOGIN, {
- user: {
- ...userData,
- redmineEndpoint: returnedValues.redmineEndpoint
- }
- }));
store.clearActions();
done();
@@ -313,8 +293,6 @@ describe('Login view', () => {
expect(queryAllByText('Something went wrong').length).toBeGreaterThan(0);
const reduxActions = store.getActions();
expect(reduxActions).toHaveLength(2);
- expect(reduxActions[0]).toEqual(notify.start(USER_LOGIN));
- expect(reduxActions[1]).toEqual(notify.nok(USER_LOGIN, new Error(`Error (${expectedError.message})`)));
store.clearActions();
loginActionSpy.mockRestore();
diff --git a/test/common/config.spec.js b/test/common/config.spec.js
index b04659a4..5f3f2244 100644
--- a/test/common/config.spec.js
+++ b/test/common/config.spec.js
@@ -29,10 +29,6 @@ describe('Config', () => {
});
it('overwrite default env data with user settings', () => {
- const storage = require('../../common/storage.js'); // eslint-disable-line
- const storageHasSpy = jest.spyOn(storage, 'has');
- const storageGetSpy = jest.spyOn(storage, 'get');
-
const config = require('../../common/config.js'); // eslint-disable-line
expect(config).toEqual({
@@ -41,10 +37,5 @@ describe('Config', () => {
NODE_ENV: 'test',
platform: process.platform
});
- expect(storageHasSpy).toHaveBeenCalledWith('settings');
- expect(storageGetSpy).toHaveBeenCalledWith('settings', {});
-
- storageHasSpy.mockRestore();
- storageGetSpy.mockRestore();
});
});
diff --git a/tsconfig.json b/tsconfig.json
index 48fb43ac..6f0da7d6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,7 +9,7 @@
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
- // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
+ "jsxImportSource": "@emotion/react", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
diff --git a/types/index.ts b/types/index.ts
index ee8995c1..2d2a6a04 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -14,24 +14,33 @@ type CreateOvermindConfigParams = {
endpoint: string;
}
-type Response = {
- data: any;
+type Response = {
+ payload: T;
success: boolean;
error?: Error;
}
// eslint-disable-next-line no-shadow
-enum StorageAction {
+enum SessionAction {
READ = 'READ',
- SAVE = 'SAVE'
+ SAVE = 'SAVE',
+ RESET = 'RESET'
+}
+
+type User = {
+ id: string;
+ firstName: string;
+ lastName: string;
+ createdOn: string;
}
export {
- StorageAction
+ SessionAction
};
export type {
IssueHeader,
CreateOvermindConfigParams,
- Response
+ Response,
+ User
};
diff --git a/yarn.lock b/yarn.lock
index 4ec4f1b1..12970102 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -941,6 +941,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.13.10":
+ version "7.16.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.0.tgz#e27b977f2e2088ba24748bf99b5e1dece64e4f0b"
+ integrity sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.15.4", "@babel/template@^7.3.3":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
@@ -1027,11 +1034,32 @@
find-root "^1.1.0"
source-map "^0.7.2"
+"@emotion/cache@^11.5.0":
+ version "11.5.0"
+ resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.5.0.tgz#a5eb78cbef8163939ee345e3ddf0af217b845e62"
+ integrity sha512-mAZ5QRpLriBtaj/k2qyrXwck6yeoz1V5lMt/jfj6igWU35yYlNKs2LziXVgvH81gnJZ+9QQNGelSsnuoAy6uIw==
+ dependencies:
+ "@emotion/memoize" "^0.7.4"
+ "@emotion/sheet" "^1.0.3"
+ "@emotion/utils" "^1.0.0"
+ "@emotion/weak-memoize" "^0.2.5"
+ stylis "^4.0.10"
+
+"@emotion/eslint-plugin@^11.5.0":
+ version "11.5.0"
+ resolved "https://registry.yarnpkg.com/@emotion/eslint-plugin/-/eslint-plugin-11.5.0.tgz#1bad513b0d557e9a2cbf91415d409113595400c2"
+ integrity sha512-AyjeBIHuFYQIMLB+t5VRqXGzMkrD98p9aCgxTxt0DGBjQa6ExDU5xvhbeSjLRClc1zIYkDmbNBbGB71RAVgqFw==
+
"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44"
integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==
+"@emotion/hash@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+ integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
"@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
@@ -1049,6 +1077,24 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b"
integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==
+"@emotion/memoize@^0.7.4":
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
+ integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
+
+"@emotion/react@^11.5.0":
+ version "11.5.0"
+ resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.5.0.tgz#19b5771bbfbda5e8517e948a2d9064810f0022bd"
+ integrity sha512-MYq/bzp3rYbee4EMBORCn4duPQfgpiEB5XzrZEBnUZAL80Qdfr7CEv/T80jwaTl/dnZmt9SnTa8NkTrwFNpLlw==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@emotion/cache" "^11.5.0"
+ "@emotion/serialize" "^1.0.2"
+ "@emotion/sheet" "^1.0.3"
+ "@emotion/utils" "^1.0.0"
+ "@emotion/weak-memoize" "^0.2.5"
+ hoist-non-react-statics "^3.3.1"
+
"@emotion/serialize@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145"
@@ -1059,6 +1105,22 @@
"@emotion/unitless" "^0.6.7"
"@emotion/utils" "^0.8.2"
+"@emotion/serialize@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
+ integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
+ dependencies:
+ "@emotion/hash" "^0.8.0"
+ "@emotion/memoize" "^0.7.4"
+ "@emotion/unitless" "^0.7.5"
+ "@emotion/utils" "^1.0.0"
+ csstype "^3.0.2"
+
+"@emotion/sheet@^1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.3.tgz#00c326cd7985c5ccb8fe2c1b592886579dcfab8f"
+ integrity sha512-YoX5GyQ4db7LpbmXHMuc8kebtBGP6nZfRC5Z13OKJMixBEwdZrJ914D6yJv/P+ZH/YY3F5s89NYX2hlZAf3SRQ==
+
"@emotion/stylis@^0.7.0":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5"
@@ -1074,7 +1136,7 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397"
integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==
-"@emotion/unitless@^0.7.4":
+"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
@@ -1084,6 +1146,16 @@
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc"
integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==
+"@emotion/utils@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
+ integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
+
+"@emotion/weak-memoize@^0.2.5":
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+ integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
"@eslint/eslintrc@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.3.tgz#41f08c597025605f672251dcc4e8be66b5ed7366"
@@ -4715,6 +4787,11 @@ emojis-list@^3.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+emotion-theming@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-11.0.0.tgz#821de3c9804cfe7bb5fda2dd12ad722a6c5bcff5"
+ integrity sha512-OhYpCGBjaLcD9c4ptwCr9SxHjfRTDqeqdzMobusJ+a/drlfnJ3AT9gmGKIhNHiXtr6626h6fsvLY22Or9CxUqw==
+
emotion@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/emotion/-/emotion-11.0.0.tgz#e33353668e72f0adea1f6fba790dc6c5b05b45d9"
@@ -6293,7 +6370,7 @@ history@^4.9.0:
tiny-warning "^1.0.0"
value-equal "^1.0.1"
-hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
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==
@@ -11358,6 +11435,11 @@ stylis@^3.5.0:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
+stylis@^4.0.10:
+ version "4.0.10"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
+ integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
+
sumchecker@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"