Merge pull request #155 from wakatime/sebas-new-ts-components
chore: implement new ts components
This commit is contained in:
2501
package-lock.json
generated
2501
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -95,8 +95,8 @@
|
||||
"jsxhint": "^0.15.1",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"mocha": "^5.0.0",
|
||||
"mocha-sinon": "^2.0.0",
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-sinon": "^2.1.2",
|
||||
"node-gyp": "^8.3.0",
|
||||
"prettier": "^2.8.2",
|
||||
"prettier-plugin-packagejson": "^2.3.0",
|
||||
@@ -104,9 +104,9 @@
|
||||
"remote-redux-devtools": "^0.5.16",
|
||||
"rimraf": "^3.0.2",
|
||||
"shelljs": "^0.8.5",
|
||||
"sinon": "^4.2.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-chrome": "^2.2.4",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-chrome": "^3.0.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../utils/test-utils';
|
||||
import MainList from './MainList';
|
||||
|
||||
type onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
describe('MainList', () => {
|
||||
let disableLogging: onClick;
|
||||
let enableLogging: onClick;
|
||||
let loggedIn: boolean;
|
||||
let loggingEnabled: boolean;
|
||||
let logoutUser: onClick;
|
||||
let totalTimeLoggedToday: string;
|
||||
@@ -14,17 +13,15 @@ describe('MainList', () => {
|
||||
disableLogging = jest.fn();
|
||||
enableLogging = jest.fn();
|
||||
loggingEnabled = false;
|
||||
loggedIn = false;
|
||||
logoutUser = jest.fn();
|
||||
totalTimeLoggedToday = '1/1/1999';
|
||||
});
|
||||
it('should render properly', () => {
|
||||
const { container } = render(
|
||||
const { container } = renderWithProviders(
|
||||
<MainList
|
||||
disableLogging={disableLogging}
|
||||
enableLogging={enableLogging}
|
||||
loggingEnabled={loggingEnabled}
|
||||
loggedIn={loggedIn}
|
||||
logoutUser={logoutUser}
|
||||
totalTimeLoggedToday={totalTimeLoggedToday}
|
||||
/>,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ReduxSelector } from '../types/store';
|
||||
import { User } from '../types/user';
|
||||
|
||||
export interface MainListProps {
|
||||
disableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
enableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
loggedIn: boolean;
|
||||
loggingEnabled: boolean;
|
||||
logoutUser: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
totalTimeLoggedToday?: string;
|
||||
@@ -15,14 +17,17 @@ const openOptionsPage = async (): Promise<void> => {
|
||||
export default function MainList({
|
||||
disableLogging,
|
||||
enableLogging,
|
||||
loggedIn,
|
||||
loggingEnabled,
|
||||
logoutUser,
|
||||
totalTimeLoggedToday,
|
||||
}: MainListProps): JSX.Element {
|
||||
const user: User | undefined = useSelector(
|
||||
(selector: ReduxSelector) => selector.currentUser.user,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loggedIn && (
|
||||
{user && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<blockquote>
|
||||
@@ -34,7 +39,7 @@ export default function MainList({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loggingEnabled && loggedIn && (
|
||||
{loggingEnabled && user && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<p>
|
||||
@@ -45,7 +50,7 @@ export default function MainList({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loggingEnabled && loggedIn && (
|
||||
{!loggingEnabled && user && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<p>
|
||||
@@ -61,7 +66,7 @@ export default function MainList({
|
||||
<i className="fa fa-fw fa-cogs"></i>
|
||||
Options
|
||||
</a>
|
||||
{loggedIn && (
|
||||
{user && (
|
||||
<div>
|
||||
<a href="#" className="list-group-item" onClick={logoutUser}>
|
||||
<i className="fa fa-fw fa-sign-out"></i>
|
||||
@@ -69,7 +74,7 @@ export default function MainList({
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!loggedIn && (
|
||||
{!user && (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
180
src/components/NavBar.tsx
Normal file
180
src/components/NavBar.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { configLogout, setApiKey } from '../reducers/configReducer';
|
||||
import { userLogout } from '../reducers/currentUser';
|
||||
import { ReduxSelector } from '../types/store';
|
||||
import { User } from '../types/user';
|
||||
import config from '../config/config';
|
||||
import apiKeyInvalid from '../utils/apiKey';
|
||||
import { fetchUserData } from '../utils/user';
|
||||
|
||||
export default function NavBar(): JSX.Element {
|
||||
const [state, setState] = useState({
|
||||
apiKey: '',
|
||||
apiKeyError: '',
|
||||
loading: false,
|
||||
});
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
const { apiKey } = await browser.storage.sync.get({ apiKey: config.apiKey });
|
||||
setState({ ...state, apiKey });
|
||||
};
|
||||
|
||||
fetch();
|
||||
}, []);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const user: User | undefined = useSelector(
|
||||
(selector: ReduxSelector) => selector.currentUser.user,
|
||||
);
|
||||
|
||||
const signedInAs = () => {
|
||||
if (user) {
|
||||
return (
|
||||
<p className="navbar-text">
|
||||
Signed in as <b>{user.full_name}</b>
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
const customRules = () => {
|
||||
if (user) {
|
||||
return (
|
||||
<li>
|
||||
<a target="_blank" href="https://wakatime.com/settings/rules" rel="noreferrer">
|
||||
<i className="fa fa-fw fa-filter"></i>
|
||||
Custom Rules
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
const dashboard = () => {
|
||||
if (user) {
|
||||
return (
|
||||
<li>
|
||||
<a target="_blank" href="https://wakatime.com/dashboard" rel="noreferrer">
|
||||
<i className="fa fa-fw fa-tachometer"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar navbar-default" role="navigation">
|
||||
<div className="container-fluid">
|
||||
<div className="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
className="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target="#bs-example-navbar-collapse-1"
|
||||
>
|
||||
<span className="sr-only">Toggle navigation</span>
|
||||
<i className="fa fa-fw fa-cogs"></i>
|
||||
</button>
|
||||
<a target="_blank" className="navbar-brand" href="https://wakatime.com" rel="noreferrer">
|
||||
WakaTime
|
||||
<img src="graphics/wakatime-logo-48.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
{signedInAs()}
|
||||
<ul className="nav navbar-nav">
|
||||
{customRules()}
|
||||
{dashboard()}
|
||||
<li className="dropdown">
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="fa fa-fw fa-info"></i>
|
||||
About
|
||||
<span className="caret"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/wakatime/chrome-wakatime/issues"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i className="fa fa-fw fa-bug"></i>
|
||||
Report an Issue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/wakatime/chrome-wakatime"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i className="fa fa-fw fa-github"></i>
|
||||
View on GitHub
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<div className="container-fluid">
|
||||
{state.apiKeyError && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{state.apiKeyError}
|
||||
</div>
|
||||
)}
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="API key"
|
||||
value={state.apiKey}
|
||||
onChange={(e) => {
|
||||
const key = e.target.value;
|
||||
const isApiKeyInvalid = apiKeyInvalid(key);
|
||||
setState({ ...state, apiKey: key, apiKeyError: isApiKeyInvalid });
|
||||
}}
|
||||
/>
|
||||
<span className="input-group-btn">
|
||||
<button
|
||||
className={`btn btn-default ${state.loading ? 'disabled' : ''}`}
|
||||
disabled={state.loading}
|
||||
type="button"
|
||||
data-loading-text="Loading..."
|
||||
onClick={async () => {
|
||||
if (state.apiKeyError === '' && state.apiKey !== '') {
|
||||
setState({ ...state, loading: true });
|
||||
await browser.storage.sync.set({ apiKey: state.apiKey });
|
||||
dispatch(configLogout());
|
||||
dispatch(userLogout());
|
||||
dispatch(setApiKey(state.apiKey));
|
||||
|
||||
await fetchUserData(state.apiKey, dispatch);
|
||||
setState({ ...state, loading: false });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
80
src/components/WakaTime.tsx
Normal file
80
src/components/WakaTime.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { ApiKeyReducer, ReduxSelector } from '../types/store';
|
||||
import config from '../config/config';
|
||||
import { fetchUserData } from '../utils/user';
|
||||
import changeExtensionState from '../utils/changeExtensionState';
|
||||
import NavBar from './NavBar';
|
||||
import MainList from './MainList';
|
||||
|
||||
export default function WakaTime(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const defaultState = {
|
||||
loggingEnabled: config.loggingEnabled,
|
||||
totalTimeLoggedToday: '0 minutes',
|
||||
};
|
||||
const [state, setState] = useState(defaultState);
|
||||
const {
|
||||
apiKey: apiKeyFromRedux,
|
||||
loggingEnabled,
|
||||
totalTimeLoggedToday,
|
||||
}: ApiKeyReducer = useSelector((selector: ReduxSelector) => selector.config);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserData(apiKeyFromRedux, dispatch);
|
||||
}, []);
|
||||
|
||||
const disableLogging = async () => {
|
||||
setState({
|
||||
...state,
|
||||
loggingEnabled: false,
|
||||
});
|
||||
|
||||
await changeExtensionState('notLogging');
|
||||
|
||||
await browser.storage.sync.set({
|
||||
loggingEnabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
const enableLogging = async () => {
|
||||
setState({
|
||||
...state,
|
||||
loggingEnabled: true,
|
||||
});
|
||||
|
||||
await changeExtensionState('allGood');
|
||||
|
||||
await browser.storage.sync.set({
|
||||
loggingEnabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
const logoutUser = async () => {
|
||||
await browser.storage.sync.set({ apiKey: '' });
|
||||
|
||||
setState(defaultState);
|
||||
|
||||
await changeExtensionState('notSignedIn');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NavBar />
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<MainList
|
||||
disableLogging={disableLogging}
|
||||
enableLogging={enableLogging}
|
||||
loggingEnabled={loggingEnabled}
|
||||
totalTimeLoggedToday={totalTimeLoggedToday}
|
||||
logoutUser={logoutUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ describe('wakatime config', () => {
|
||||
"type": "success",
|
||||
},
|
||||
},
|
||||
"apiKey": "",
|
||||
"colors": {
|
||||
"allGood": "",
|
||||
"lightTheme": "white",
|
||||
|
||||
@@ -47,6 +47,10 @@ interface Tooltips {
|
||||
|
||||
export interface Config {
|
||||
alert: Alert;
|
||||
/**
|
||||
* API key use to query wakatime api
|
||||
*/
|
||||
apiKey: '';
|
||||
colors: Colors;
|
||||
/**
|
||||
* Url from which to detect if the user is logged in
|
||||
@@ -104,6 +108,8 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
|
||||
apiKey: '',
|
||||
|
||||
colors: {
|
||||
allGood: '',
|
||||
lightTheme: 'white',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import moment from 'moment';
|
||||
import { Tabs } from 'webextension-polyfill-ts';
|
||||
import { User } from '../types/user';
|
||||
import { AxiosUserResponse, User } from '../types/user';
|
||||
import config from '../config/config';
|
||||
import { SummariesPayload, GrandTotal } from '../types/summaries';
|
||||
|
||||
@@ -10,15 +10,18 @@ class WakaTimeCore {
|
||||
constructor() {
|
||||
this.tabsWithDevtoolsOpen = [];
|
||||
}
|
||||
|
||||
setTabsWithDevtoolsOpen(tabs: Tabs.Tab[]): void {
|
||||
this.tabsWithDevtoolsOpen = tabs;
|
||||
}
|
||||
async getTotalTimeLoggedToday(): Promise<GrandTotal> {
|
||||
|
||||
async getTotalTimeLoggedToday(api_key = ''): Promise<GrandTotal> {
|
||||
const today = moment().format('YYYY-MM-DD');
|
||||
const summariesAxiosPayload: AxiosResponse<SummariesPayload> = await axios.get(
|
||||
config.summariesApiUrl,
|
||||
{
|
||||
data: {
|
||||
params: {
|
||||
api_key,
|
||||
end: today,
|
||||
start: today,
|
||||
},
|
||||
@@ -26,9 +29,22 @@ class WakaTimeCore {
|
||||
);
|
||||
return summariesAxiosPayload.data.data[0].grand_total;
|
||||
}
|
||||
async checkAuth(): Promise<User> {
|
||||
const userPayload: AxiosResponse<User> = await axios.get(config.currentUserApiUrl);
|
||||
return userPayload.data;
|
||||
|
||||
async checkAuth(api_key = ''): Promise<User> {
|
||||
const userPayload: AxiosResponse<AxiosUserResponse> = await axios.get(
|
||||
config.currentUserApiUrl,
|
||||
{ params: { api_key } },
|
||||
);
|
||||
return userPayload.data.data;
|
||||
}
|
||||
|
||||
async recordHeartbeat(): Promise<void> {
|
||||
const items = await browser.storage.sync.get({
|
||||
blacklist: '',
|
||||
loggingEnabled: config.loggingEnabled,
|
||||
loggingStyle: config.loggingStyle,
|
||||
whitelist: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,2 @@
|
||||
// // Create a connection to the background page
|
||||
// const backgroundPageConnection = browser.runtime.connect({
|
||||
// name: 'devtools-page',
|
||||
// });
|
||||
|
||||
// // Send a message to background page with the current active tabId
|
||||
// backgroundPageConnection.postMessage({
|
||||
// name: 'init',
|
||||
// tabId: browser.devtools.inspectedWindow.tabId,
|
||||
// });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
browser.devtools.panels.create('Wakatime', 'test.png', 'WakatimeDevPanel.html');
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import WakaTime from './components/WakaTime';
|
||||
import createStore from './stores/createStore';
|
||||
import checkCurrentUser from './utils/checkCurrentUser';
|
||||
const container = document.getElementById('wakatime');
|
||||
|
||||
/* This is a fix for Bootstrap requiring jQuery */
|
||||
global.jQuery = require('jquery');
|
||||
require('bootstrap');
|
||||
|
||||
const container = document.getElementById('wakatime');
|
||||
const root = createRoot(container!);
|
||||
const store = createStore('WakaTime-Options');
|
||||
checkCurrentUser(store)(30 * 1000);
|
||||
|
||||
@@ -12,10 +18,9 @@ const openOptions = async (): Promise<void> => {
|
||||
await browser.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<h1>POPUP GO HERE</h1>
|
||||
<WakaTime />
|
||||
<div onClick={openOptions}>Open options</div>
|
||||
</Provider>,
|
||||
container,
|
||||
);
|
||||
|
||||
50
src/reducers/configReducer.ts
Normal file
50
src/reducers/configReducer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import config from '../config/config';
|
||||
import { ApiKeyReducer } from '../types/store';
|
||||
|
||||
interface SetApiKeyAction {
|
||||
payload: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SetLoggingEnabledAction {
|
||||
payload: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SetTotalTimeLoggedTodayAction {
|
||||
payload: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const initialConfigState: ApiKeyReducer = {
|
||||
apiKey: '',
|
||||
loggingEnabled: config.loggingEnabled,
|
||||
totalTimeLoggedToday: '0 minutes',
|
||||
};
|
||||
|
||||
const apiKeySlice = createSlice({
|
||||
initialState: initialConfigState,
|
||||
name: 'configReducer',
|
||||
reducers: {
|
||||
configLogout: (state) => {
|
||||
state.apiKey = '';
|
||||
state.loggingEnabled = config.loggingEnabled;
|
||||
state.totalTimeLoggedToday = '0 minutes';
|
||||
},
|
||||
setApiKey: (state, action: SetApiKeyAction) => {
|
||||
state.apiKey = action.payload;
|
||||
},
|
||||
setLoggingEnabled: (state, action: SetLoggingEnabledAction) => {
|
||||
state.loggingEnabled = action.payload;
|
||||
},
|
||||
setTotalTimeLoggedToday: (state, action: SetTotalTimeLoggedTodayAction) => {
|
||||
state.totalTimeLoggedToday = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const actions = apiKeySlice.actions;
|
||||
export const { configLogout, setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } =
|
||||
apiKeySlice.actions;
|
||||
export default apiKeySlice.reducer;
|
||||
@@ -1,21 +1,26 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { User, UserPayload } from '../types/user';
|
||||
import { CurrentUser, User, UserPayload } from '../types/user';
|
||||
import config from '../config/config';
|
||||
|
||||
interface setUserAction {
|
||||
payload: User | undefined;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type NameType = 'currentUser';
|
||||
export const name: NameType = 'currentUser';
|
||||
|
||||
export const fetchCurrentUser = createAsyncThunk<User, undefined>(`[${name}]`, async () => {
|
||||
const userPayload: AxiosResponse<UserPayload> = await axios.get(config.currentUserApiUrl);
|
||||
return userPayload.data.data;
|
||||
});
|
||||
export const fetchCurrentUser = createAsyncThunk<User, string>(
|
||||
`[${name}]`,
|
||||
async (api_key = '') => {
|
||||
const userPayload: AxiosResponse<UserPayload> = await axios.get(config.currentUserApiUrl, {
|
||||
params: { api_key },
|
||||
});
|
||||
return userPayload.data.data;
|
||||
},
|
||||
);
|
||||
|
||||
export interface CurrentUser {
|
||||
error?: unknown;
|
||||
pending?: boolean;
|
||||
user?: User;
|
||||
}
|
||||
export const initialState: CurrentUser = {};
|
||||
|
||||
const currentUser = createSlice({
|
||||
@@ -30,8 +35,16 @@ const currentUser = createSlice({
|
||||
},
|
||||
initialState,
|
||||
name,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
setUser: (state, action: setUserAction) => {
|
||||
state.user = action.payload;
|
||||
},
|
||||
userLogout: (state) => {
|
||||
state.user = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const actions = currentUser.actions;
|
||||
export const { setUser, userLogout } = currentUser.actions;
|
||||
export default currentUser.reducer;
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { configureStore, Store } from '@reduxjs/toolkit';
|
||||
import { configureStore, Store, combineReducers } from '@reduxjs/toolkit';
|
||||
import { logger } from 'redux-logger';
|
||||
import { reduxBatch } from '@manaflair/redux-batch';
|
||||
import devToolsEnhancer from 'remote-redux-devtools';
|
||||
import currentUserReducer, {
|
||||
initialState as InitalCurrentUser,
|
||||
CurrentUser,
|
||||
} from '../reducers/currentUser';
|
||||
import currentUserReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser';
|
||||
import configReducer, { initialConfigState } from '../reducers/configReducer';
|
||||
import isProd from '../utils/isProd';
|
||||
|
||||
export interface RootState {
|
||||
currentUser: CurrentUser;
|
||||
}
|
||||
// Create the root reducer separately so we can extract the RootState type
|
||||
const rootReducer = combineReducers({
|
||||
config: configReducer,
|
||||
currentUser: currentUserReducer,
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
const preloadedState: RootState = {
|
||||
config: initialConfigState,
|
||||
currentUser: InitalCurrentUser,
|
||||
};
|
||||
|
||||
export type RootStore = Store<RootState>;
|
||||
export default (appName: string): RootStore => {
|
||||
export default (appName: string): Store<RootState> => {
|
||||
const enhancers = [];
|
||||
enhancers.push(reduxBatch);
|
||||
if (!isProd()) {
|
||||
@@ -30,9 +32,7 @@ export default (appName: string): RootStore => {
|
||||
enhancers,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
|
||||
preloadedState,
|
||||
reducer: {
|
||||
currentUser: currentUserReducer,
|
||||
},
|
||||
reducer: rootReducer,
|
||||
});
|
||||
|
||||
return store;
|
||||
|
||||
12
src/types/store.ts
Normal file
12
src/types/store.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CurrentUser } from './user';
|
||||
|
||||
export interface ApiKeyReducer {
|
||||
apiKey: string;
|
||||
loggingEnabled: boolean;
|
||||
totalTimeLoggedToday: string;
|
||||
}
|
||||
|
||||
export interface ReduxSelector {
|
||||
config: ApiKeyReducer;
|
||||
currentUser: CurrentUser;
|
||||
}
|
||||
@@ -4,6 +4,10 @@ export interface UserPayload {
|
||||
data: User;
|
||||
}
|
||||
|
||||
export interface AxiosUserResponse {
|
||||
data: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
bio: null;
|
||||
color_scheme: string;
|
||||
@@ -42,3 +46,9 @@ export interface User {
|
||||
weekday_start: number;
|
||||
writes_only: boolean;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
error?: unknown;
|
||||
pending?: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
10
src/utils/apiKey.ts
Normal file
10
src/utils/apiKey.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function apiKeyInvalid(key?: string): string {
|
||||
const err = 'Invalid api key... check https://wakatime.com/settings for your key';
|
||||
if (!key) return err;
|
||||
const re = new RegExp(
|
||||
'^(waka_)?[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$',
|
||||
'i',
|
||||
);
|
||||
if (!re.test(key)) return err;
|
||||
return '';
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { RootStore } from '../stores/createStore';
|
||||
import { Store } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../stores/createStore';
|
||||
import { fetchCurrentUser } from '../reducers/currentUser';
|
||||
import { ReduxSelector } from '../types/store';
|
||||
|
||||
type unsub = () => void;
|
||||
export default (store: RootStore) =>
|
||||
export default (store: Store<RootState>) =>
|
||||
(time: number): unsub => {
|
||||
const fetchUser = () => {
|
||||
const apiKey: string = (store.getState() as ReduxSelector).config.apiKey;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
store.dispatch(fetchCurrentUser());
|
||||
store.dispatch(fetchCurrentUser(apiKey));
|
||||
};
|
||||
fetchUser();
|
||||
const timeout = setInterval(fetchUser, time);
|
||||
|
||||
43
src/utils/test-utils.tsx
Normal file
43
src/utils/test-utils.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { RenderOptions } from '@testing-library/react';
|
||||
import { combineReducers, configureStore, Store } from '@reduxjs/toolkit';
|
||||
import type { PreloadedState } from '@reduxjs/toolkit';
|
||||
import { Provider } from 'react-redux';
|
||||
import { RootState } from '../stores/createStore';
|
||||
|
||||
// As a basic setup, import your same slice reducers
|
||||
import userReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser';
|
||||
import configReducer, { initialConfigState } from '../reducers/configReducer';
|
||||
|
||||
// This type interface extends the default options for render from RTL, as well
|
||||
// as allows the user to specify other things such as initialState, store.
|
||||
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
|
||||
preloadedState?: PreloadedState<RootState>;
|
||||
store?: Store<RootState>;
|
||||
}
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
config: configReducer,
|
||||
currentUser: userReducer,
|
||||
});
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
{
|
||||
preloadedState = {
|
||||
config: initialConfigState,
|
||||
currentUser: InitalCurrentUser,
|
||||
},
|
||||
// Automatically create a store instance if no store was passed in
|
||||
store = configureStore({ preloadedState, reducer: rootReducer }),
|
||||
...renderOptions
|
||||
}: ExtendedRenderOptions = {},
|
||||
): any {
|
||||
function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>): JSX.Element {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
|
||||
// Return an object with the store and all of RTL's query functions
|
||||
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
|
||||
}
|
||||
45
src/utils/user.ts
Normal file
45
src/utils/user.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
|
||||
import { setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } from '../reducers/configReducer';
|
||||
import config from '../config/config';
|
||||
import WakaTimeCore from '../core/WakaTimeCore';
|
||||
import { setUser } from '../reducers/currentUser';
|
||||
import changeExtensionState from './changeExtensionState';
|
||||
|
||||
export const fetchUserData = async (
|
||||
apiKey: string,
|
||||
dispatch: Dispatch<AnyAction>,
|
||||
): Promise<void> => {
|
||||
if (!apiKey) {
|
||||
const storage = await browser.storage.sync.get({
|
||||
apiKey: config.apiKey,
|
||||
});
|
||||
apiKey = storage.apiKey as string;
|
||||
dispatch(setApiKey(apiKey));
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
await changeExtensionState('notSignedIn');
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, totalTimeLoggedTodayResponse, items] = await Promise.all([
|
||||
WakaTimeCore.checkAuth(apiKey),
|
||||
WakaTimeCore.getTotalTimeLoggedToday(apiKey),
|
||||
browser.storage.sync.get({ loggingEnabled: config.loggingEnabled }),
|
||||
]);
|
||||
dispatch(setUser(data));
|
||||
|
||||
if (items.loggingEnabled === true) {
|
||||
await changeExtensionState('allGood');
|
||||
} else {
|
||||
await changeExtensionState('notLogging');
|
||||
}
|
||||
|
||||
dispatch(setLoggingEnabled(items.loggingEnabled as boolean));
|
||||
dispatch(setTotalTimeLoggedToday(totalTimeLoggedTodayResponse.text));
|
||||
|
||||
await WakaTimeCore.recordHeartbeat();
|
||||
} catch (err: unknown) {
|
||||
await changeExtensionState('notSignedIn');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user