Merge pull request #155 from wakatime/sebas-new-ts-components
chore: implement new ts components
This commit is contained in:
2477
package-lock.json
generated
2477
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",
|
"jsxhint": "^0.15.1",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.1.0",
|
"lint-staged": "^13.1.0",
|
||||||
"mocha": "^5.0.0",
|
"mocha": "^10.2.0",
|
||||||
"mocha-sinon": "^2.0.0",
|
"mocha-sinon": "^2.1.2",
|
||||||
"node-gyp": "^8.3.0",
|
"node-gyp": "^8.3.0",
|
||||||
"prettier": "^2.8.2",
|
"prettier": "^2.8.2",
|
||||||
"prettier-plugin-packagejson": "^2.3.0",
|
"prettier-plugin-packagejson": "^2.3.0",
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
"remote-redux-devtools": "^0.5.16",
|
"remote-redux-devtools": "^0.5.16",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"shelljs": "^0.8.5",
|
"shelljs": "^0.8.5",
|
||||||
"sinon": "^4.2.2",
|
"sinon": "^15.0.1",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^3.7.0",
|
||||||
"sinon-chrome": "^2.2.4",
|
"sinon-chrome": "^3.0.1",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { renderWithProviders } from '../utils/test-utils';
|
||||||
import MainList from './MainList';
|
import MainList from './MainList';
|
||||||
|
|
||||||
type onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
type onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||||
describe('MainList', () => {
|
describe('MainList', () => {
|
||||||
let disableLogging: onClick;
|
let disableLogging: onClick;
|
||||||
let enableLogging: onClick;
|
let enableLogging: onClick;
|
||||||
let loggedIn: boolean;
|
|
||||||
let loggingEnabled: boolean;
|
let loggingEnabled: boolean;
|
||||||
let logoutUser: onClick;
|
let logoutUser: onClick;
|
||||||
let totalTimeLoggedToday: string;
|
let totalTimeLoggedToday: string;
|
||||||
@@ -14,17 +13,15 @@ describe('MainList', () => {
|
|||||||
disableLogging = jest.fn();
|
disableLogging = jest.fn();
|
||||||
enableLogging = jest.fn();
|
enableLogging = jest.fn();
|
||||||
loggingEnabled = false;
|
loggingEnabled = false;
|
||||||
loggedIn = false;
|
|
||||||
logoutUser = jest.fn();
|
logoutUser = jest.fn();
|
||||||
totalTimeLoggedToday = '1/1/1999';
|
totalTimeLoggedToday = '1/1/1999';
|
||||||
});
|
});
|
||||||
it('should render properly', () => {
|
it('should render properly', () => {
|
||||||
const { container } = render(
|
const { container } = renderWithProviders(
|
||||||
<MainList
|
<MainList
|
||||||
disableLogging={disableLogging}
|
disableLogging={disableLogging}
|
||||||
enableLogging={enableLogging}
|
enableLogging={enableLogging}
|
||||||
loggingEnabled={loggingEnabled}
|
loggingEnabled={loggingEnabled}
|
||||||
loggedIn={loggedIn}
|
|
||||||
logoutUser={logoutUser}
|
logoutUser={logoutUser}
|
||||||
totalTimeLoggedToday={totalTimeLoggedToday}
|
totalTimeLoggedToday={totalTimeLoggedToday}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { ReduxSelector } from '../types/store';
|
||||||
|
import { User } from '../types/user';
|
||||||
|
|
||||||
export interface MainListProps {
|
export interface MainListProps {
|
||||||
disableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
disableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||||
enableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
enableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||||
loggedIn: boolean;
|
|
||||||
loggingEnabled: boolean;
|
loggingEnabled: boolean;
|
||||||
logoutUser: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
logoutUser: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||||
totalTimeLoggedToday?: string;
|
totalTimeLoggedToday?: string;
|
||||||
@@ -15,14 +17,17 @@ const openOptionsPage = async (): Promise<void> => {
|
|||||||
export default function MainList({
|
export default function MainList({
|
||||||
disableLogging,
|
disableLogging,
|
||||||
enableLogging,
|
enableLogging,
|
||||||
loggedIn,
|
|
||||||
loggingEnabled,
|
loggingEnabled,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
totalTimeLoggedToday,
|
totalTimeLoggedToday,
|
||||||
}: MainListProps): JSX.Element {
|
}: MainListProps): JSX.Element {
|
||||||
|
const user: User | undefined = useSelector(
|
||||||
|
(selector: ReduxSelector) => selector.currentUser.user,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loggedIn && (
|
{user && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<blockquote>
|
<blockquote>
|
||||||
@@ -34,7 +39,7 @@ export default function MainList({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{loggingEnabled && loggedIn && (
|
{loggingEnabled && user && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<p>
|
<p>
|
||||||
@@ -45,7 +50,7 @@ export default function MainList({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loggingEnabled && loggedIn && (
|
{!loggingEnabled && user && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<p>
|
<p>
|
||||||
@@ -61,7 +66,7 @@ export default function MainList({
|
|||||||
<i className="fa fa-fw fa-cogs"></i>
|
<i className="fa fa-fw fa-cogs"></i>
|
||||||
Options
|
Options
|
||||||
</a>
|
</a>
|
||||||
{loggedIn && (
|
{user && (
|
||||||
<div>
|
<div>
|
||||||
<a href="#" className="list-group-item" onClick={logoutUser}>
|
<a href="#" className="list-group-item" onClick={logoutUser}>
|
||||||
<i className="fa fa-fw fa-sign-out"></i>
|
<i className="fa fa-fw fa-sign-out"></i>
|
||||||
@@ -69,7 +74,7 @@ export default function MainList({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loggedIn && (
|
{!user && (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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",
|
"type": "success",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"apiKey": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
"allGood": "",
|
"allGood": "",
|
||||||
"lightTheme": "white",
|
"lightTheme": "white",
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ interface Tooltips {
|
|||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
alert: Alert;
|
alert: Alert;
|
||||||
|
/**
|
||||||
|
* API key use to query wakatime api
|
||||||
|
*/
|
||||||
|
apiKey: '';
|
||||||
colors: Colors;
|
colors: Colors;
|
||||||
/**
|
/**
|
||||||
* Url from which to detect if the user is logged in
|
* Url from which to detect if the user is logged in
|
||||||
@@ -104,6 +108,8 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
apiKey: '',
|
||||||
|
|
||||||
colors: {
|
colors: {
|
||||||
allGood: '',
|
allGood: '',
|
||||||
lightTheme: 'white',
|
lightTheme: 'white',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Tabs } from 'webextension-polyfill-ts';
|
import { Tabs } from 'webextension-polyfill-ts';
|
||||||
import { User } from '../types/user';
|
import { AxiosUserResponse, User } from '../types/user';
|
||||||
import config from '../config/config';
|
import config from '../config/config';
|
||||||
import { SummariesPayload, GrandTotal } from '../types/summaries';
|
import { SummariesPayload, GrandTotal } from '../types/summaries';
|
||||||
|
|
||||||
@@ -10,15 +10,18 @@ class WakaTimeCore {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.tabsWithDevtoolsOpen = [];
|
this.tabsWithDevtoolsOpen = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setTabsWithDevtoolsOpen(tabs: Tabs.Tab[]): void {
|
setTabsWithDevtoolsOpen(tabs: Tabs.Tab[]): void {
|
||||||
this.tabsWithDevtoolsOpen = tabs;
|
this.tabsWithDevtoolsOpen = tabs;
|
||||||
}
|
}
|
||||||
async getTotalTimeLoggedToday(): Promise<GrandTotal> {
|
|
||||||
|
async getTotalTimeLoggedToday(api_key = ''): Promise<GrandTotal> {
|
||||||
const today = moment().format('YYYY-MM-DD');
|
const today = moment().format('YYYY-MM-DD');
|
||||||
const summariesAxiosPayload: AxiosResponse<SummariesPayload> = await axios.get(
|
const summariesAxiosPayload: AxiosResponse<SummariesPayload> = await axios.get(
|
||||||
config.summariesApiUrl,
|
config.summariesApiUrl,
|
||||||
{
|
{
|
||||||
data: {
|
params: {
|
||||||
|
api_key,
|
||||||
end: today,
|
end: today,
|
||||||
start: today,
|
start: today,
|
||||||
},
|
},
|
||||||
@@ -26,9 +29,22 @@ class WakaTimeCore {
|
|||||||
);
|
);
|
||||||
return summariesAxiosPayload.data.data[0].grand_total;
|
return summariesAxiosPayload.data.data[0].grand_total;
|
||||||
}
|
}
|
||||||
async checkAuth(): Promise<User> {
|
|
||||||
const userPayload: AxiosResponse<User> = await axios.get(config.currentUserApiUrl);
|
async checkAuth(api_key = ''): Promise<User> {
|
||||||
return userPayload.data;
|
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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
browser.devtools.panels.create('Wakatime', 'test.png', 'WakatimeDevPanel.html');
|
browser.devtools.panels.create('Wakatime', 'test.png', 'WakatimeDevPanel.html');
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import WakaTime from './components/WakaTime';
|
||||||
import createStore from './stores/createStore';
|
import createStore from './stores/createStore';
|
||||||
import checkCurrentUser from './utils/checkCurrentUser';
|
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');
|
const store = createStore('WakaTime-Options');
|
||||||
checkCurrentUser(store)(30 * 1000);
|
checkCurrentUser(store)(30 * 1000);
|
||||||
|
|
||||||
@@ -12,10 +18,9 @@ const openOptions = async (): Promise<void> => {
|
|||||||
await browser.runtime.openOptionsPage();
|
await browser.runtime.openOptionsPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<h1>POPUP GO HERE</h1>
|
<WakaTime />
|
||||||
<div onClick={openOptions}>Open options</div>
|
<div onClick={openOptions}>Open options</div>
|
||||||
</Provider>,
|
</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 { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { User, UserPayload } from '../types/user';
|
import { CurrentUser, User, UserPayload } from '../types/user';
|
||||||
import config from '../config/config';
|
import config from '../config/config';
|
||||||
|
|
||||||
|
interface setUserAction {
|
||||||
|
payload: User | undefined;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
type NameType = 'currentUser';
|
type NameType = 'currentUser';
|
||||||
export const name: NameType = 'currentUser';
|
export const name: NameType = 'currentUser';
|
||||||
|
|
||||||
export const fetchCurrentUser = createAsyncThunk<User, undefined>(`[${name}]`, async () => {
|
export const fetchCurrentUser = createAsyncThunk<User, string>(
|
||||||
const userPayload: AxiosResponse<UserPayload> = await axios.get(config.currentUserApiUrl);
|
`[${name}]`,
|
||||||
return userPayload.data.data;
|
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 = {};
|
export const initialState: CurrentUser = {};
|
||||||
|
|
||||||
const currentUser = createSlice({
|
const currentUser = createSlice({
|
||||||
@@ -30,8 +35,16 @@ const currentUser = createSlice({
|
|||||||
},
|
},
|
||||||
initialState,
|
initialState,
|
||||||
name,
|
name,
|
||||||
reducers: {},
|
reducers: {
|
||||||
|
setUser: (state, action: setUserAction) => {
|
||||||
|
state.user = action.payload;
|
||||||
|
},
|
||||||
|
userLogout: (state) => {
|
||||||
|
state.user = undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actions = currentUser.actions;
|
export const actions = currentUser.actions;
|
||||||
|
export const { setUser, userLogout } = currentUser.actions;
|
||||||
export default currentUser.reducer;
|
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 { logger } from 'redux-logger';
|
||||||
import { reduxBatch } from '@manaflair/redux-batch';
|
import { reduxBatch } from '@manaflair/redux-batch';
|
||||||
import devToolsEnhancer from 'remote-redux-devtools';
|
import devToolsEnhancer from 'remote-redux-devtools';
|
||||||
import currentUserReducer, {
|
import currentUserReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser';
|
||||||
initialState as InitalCurrentUser,
|
import configReducer, { initialConfigState } from '../reducers/configReducer';
|
||||||
CurrentUser,
|
|
||||||
} from '../reducers/currentUser';
|
|
||||||
import isProd from '../utils/isProd';
|
import isProd from '../utils/isProd';
|
||||||
|
|
||||||
export interface RootState {
|
// Create the root reducer separately so we can extract the RootState type
|
||||||
currentUser: CurrentUser;
|
const rootReducer = combineReducers({
|
||||||
}
|
config: configReducer,
|
||||||
|
currentUser: currentUserReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
const preloadedState: RootState = {
|
const preloadedState: RootState = {
|
||||||
|
config: initialConfigState,
|
||||||
currentUser: InitalCurrentUser,
|
currentUser: InitalCurrentUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootStore = Store<RootState>;
|
export default (appName: string): Store<RootState> => {
|
||||||
export default (appName: string): RootStore => {
|
|
||||||
const enhancers = [];
|
const enhancers = [];
|
||||||
enhancers.push(reduxBatch);
|
enhancers.push(reduxBatch);
|
||||||
if (!isProd()) {
|
if (!isProd()) {
|
||||||
@@ -30,9 +32,7 @@ export default (appName: string): RootStore => {
|
|||||||
enhancers,
|
enhancers,
|
||||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
|
||||||
preloadedState,
|
preloadedState,
|
||||||
reducer: {
|
reducer: rootReducer,
|
||||||
currentUser: currentUserReducer,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return store;
|
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;
|
data: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AxiosUserResponse {
|
||||||
|
data: User;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
bio: null;
|
bio: null;
|
||||||
color_scheme: string;
|
color_scheme: string;
|
||||||
@@ -42,3 +46,9 @@ export interface User {
|
|||||||
weekday_start: number;
|
weekday_start: number;
|
||||||
writes_only: boolean;
|
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 { fetchCurrentUser } from '../reducers/currentUser';
|
||||||
|
import { ReduxSelector } from '../types/store';
|
||||||
|
|
||||||
type unsub = () => void;
|
type unsub = () => void;
|
||||||
export default (store: RootStore) =>
|
export default (store: Store<RootState>) =>
|
||||||
(time: number): unsub => {
|
(time: number): unsub => {
|
||||||
const fetchUser = () => {
|
const fetchUser = () => {
|
||||||
|
const apiKey: string = (store.getState() as ReduxSelector).config.apiKey;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
store.dispatch(fetchCurrentUser());
|
store.dispatch(fetchCurrentUser(apiKey));
|
||||||
};
|
};
|
||||||
fetchUser();
|
fetchUser();
|
||||||
const timeout = setInterval(fetchUser, time);
|
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