Redux toolkit (#115)

* add @redux/toolkit

* bump react version to allow for hooks

* add remote-redux devtools to help track extension state

* add remote-redux server to allow for remote state viewing

* setup react-redux for current user

* setup watch mode for running redux remote dev watch to options

* move screenshots
This commit is contained in:
Vu Nguyen
2021-01-22 16:37:59 -08:00
committed by GitHub
parent 0c39fbbc79
commit 8ade367b3f
20 changed files with 1859 additions and 22 deletions

View File

@@ -2,4 +2,5 @@ coverage
public public
vendor vendor
assets/less assets/less
dist

View File

@@ -4,3 +4,11 @@
- npm 6.7.0 - npm 6.7.0
It is suggested you use [nvm](https://github.com/nvm-sh/nvm) to manage your node version It is suggested you use [nvm](https://github.com/nvm-sh/nvm) to manage your node version
It is suggested to install this globally[@xarc/run-cli](https://www.npmjs.com/package/@xarc/run-cli)
This will allow you to run varios tasks
![xrun auto complete](./screenshots/xrun-autocomplete.png)
In devmode you can open [local remote devtools](http://localhost:8000)
![remote redux devtools](./screenshots/remote-redux-devtools.png)

1714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"lint": "clap lint", "lint": "clap lint",
"start": "clap build", "start": "clap build",
"test": "clap test", "test": "clap test",
"validate": "npm ls" "validate": "npm ls",
"watch": "clap watch"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -25,15 +26,19 @@
] ]
}, },
"dependencies": { "dependencies": {
"@manaflair/redux-batch": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"create-react-class": "^15.6.3", "create-react-class": "^15.6.3",
"font-awesome": "4.6.3", "font-awesome": "4.6.3",
"jquery": "^3.0.0", "jquery": "^3.0.0",
"moment": "^2.13.0", "moment": "^2.13.0",
"react": "^16.2.0", "react": "^16.14.0",
"react-dom": "^16.2.0", "react-dom": "^16.14.0",
"react-redux": "^7.2.2",
"react-transition-group": "^1.0.0", "react-transition-group": "^1.0.0",
"redux-logger": "^3.0.6",
"webextension-polyfill-ts": "^0.22.0" "webextension-polyfill-ts": "^0.22.0"
}, },
"devDependencies": { "devDependencies": {
@@ -54,6 +59,9 @@
"@types/node": "^14.14.20", "@types/node": "^14.14.20",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-redux": "^7.1.15",
"@types/redux-logger": "^3.0.8",
"@types/remote-redux-devtools": "^0.5.4",
"@types/shelljs": "^0.8.8", "@types/shelljs": "^0.8.8",
"@typescript-eslint/eslint-plugin": "^4.13.0", "@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0", "@typescript-eslint/parser": "^4.13.0",
@@ -94,6 +102,8 @@
"prettier": "^2.2.1", "prettier": "^2.2.1",
"prettier-plugin-packagejson": "^2.2.9", "prettier-plugin-packagejson": "^2.2.9",
"prettier-plugin-sort-json": "0.0.1", "prettier-plugin-sort-json": "0.0.1",
"remote-redux-devtools": "^0.5.16",
"remotedev-server": "^0.3.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"sinon": "^4.2.2", "sinon": "^4.2.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SuccessOrFailType } from '../config'; import { SuccessOrFailType } from '../config/config';
interface AlertProps { interface AlertProps {
text: string; text: string;
type: SuccessOrFailType; type: SuccessOrFailType;

View File

@@ -2,7 +2,7 @@ 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 { User } from '../types/user';
import config from '../config'; import config from '../config/config';
import { SummariesPayload, GrandTotal } from '../types/summaries'; import { SummariesPayload, GrandTotal } from '../types/summaries';
class WakaTimeCore { class WakaTimeCore {

View File

@@ -1,14 +1,21 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import createStore from './stores/createStore';
import checkCurrentUser from './utils/checkCurrentUser';
const container = document.getElementById('wakatime'); const container = document.getElementById('wakatime');
const store = createStore('WakaTime-Options');
checkCurrentUser(store)(30 * 1000);
const openOptions = async (): Promise<void> => { const openOptions = async (): Promise<void> => {
await browser.runtime.openOptionsPage(); await browser.runtime.openOptionsPage();
}; };
ReactDOM.render( ReactDOM.render(
<> <Provider store={store}>
<h1>POPUP GO HERE</h1> <h1>POPUP GO HERE</h1>
<div onClick={openOptions}>Open options</div> <div onClick={openOptions}>Open options</div>
</>, </Provider>,
container, container,
); );

View File

@@ -0,0 +1,37 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios, { AxiosResponse } from 'axios';
import { User, UserPayload } from '../types/user';
import config from '../config/config';
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 interface CurrentUser {
error?: unknown;
pending?: boolean;
user?: User;
}
export const initialState: CurrentUser = {};
const currentUser = createSlice({
extraReducers: (builder) => {
builder.addCase(fetchCurrentUser.fulfilled, (state, { payload }) => {
state.user = payload;
});
builder.addCase(fetchCurrentUser.rejected, (state, { error }) => {
state.user = undefined;
state.error = error;
});
},
initialState,
name,
reducers: {},
});
export const actions = currentUser.actions;
export default currentUser.reducer;

41
src/stores/createStore.ts Normal file
View File

@@ -0,0 +1,41 @@
import { configureStore, Store } 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 isProd from '../utils/isProd';
export interface RootState {
currentUser: CurrentUser;
}
const preloadedState: RootState = {
currentUser: InitalCurrentUser,
};
export type RootStore = Store<RootState>;
export default (appName: string): RootStore => {
const enhancers = [reduxBatch];
if (!isProd()) {
enhancers.push(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
devToolsEnhancer({ hostname: 'localhost', name: appName, port: 8000, realtime: true }),
);
}
const store = configureStore({
devTools: true,
enhancers,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
preloadedState,
reducer: {
currentUser: currentUserReducer,
},
});
return store;
};

View File

@@ -1,5 +1,5 @@
import { browser } from 'webextension-polyfill-ts'; import { browser } from 'webextension-polyfill-ts';
import config from '../config'; import config from '../config/config';
type ColorIconTypes = 'gray' | 'red' | 'white' | ''; type ColorIconTypes = 'gray' | 'red' | 'white' | '';

View File

@@ -1,4 +1,4 @@
import config, { ApiStates } from '../config'; import config, { ApiStates } from '../config/config';
import changeExtensionIcon from './changeExtensionIcon'; import changeExtensionIcon from './changeExtensionIcon';
import changeExtensionTooltip from './changeExtensionTooltip'; import changeExtensionTooltip from './changeExtensionTooltip';

View File

@@ -1,5 +1,5 @@
import { browser } from 'webextension-polyfill-ts'; import { browser } from 'webextension-polyfill-ts';
import config from '../config'; import config from '../config/config';
/** /**
* It changes the extension title * It changes the extension title

View File

@@ -0,0 +1,16 @@
import { RootStore } from '../stores/createStore';
import { fetchCurrentUser } from '../reducers/currentUser';
type unsub = () => void;
export default (store: RootStore) => (time: number): unsub => {
const fetchUser = () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
store.dispatch(fetchCurrentUser());
};
fetchUser();
const timeout = setInterval(fetchUser, time);
return () => {
clearInterval(timeout);
};
};

1
src/utils/isProd.ts Normal file
View File

@@ -0,0 +1 @@
export default (): boolean => process.env.NODE_ENV !== 'development';

View File

@@ -19,14 +19,14 @@ const browserPolyfill = join(
); );
const getConfigByBrowser = (isProd: boolean, browser: BrowserTypes): webpack.Configuration => { const getConfigByBrowser = (isProd: boolean, browser: BrowserTypes): webpack.Configuration => {
const cfg: webpack.Configuration = { const cfg: webpack.Configuration = {
devtool: isProd ? 'none' : 'inline-source-map', devtool: 'inline-source-map',
entry: { entry: {
background: [join(srcFolder, 'background.ts')], background: [join(srcFolder, 'background.ts')],
devtools: [join(srcFolder, 'devtools.ts')], devtools: [join(srcFolder, 'devtools.ts')],
options: [join(srcFolder, 'options.tsx')], options: [join(srcFolder, 'options.tsx')],
popup: [join(srcFolder, 'popup.tsx')], popup: [join(srcFolder, 'popup.tsx')],
}, },
mode: isProd ? 'production' : 'development', // mode: isProd ? 'production' : 'development',
module: { module: {
rules: [ rules: [
{ {
@@ -75,7 +75,10 @@ const getConfigByBrowser = (isProd: boolean, browser: BrowserTypes): webpack.Con
}; };
return cfg; return cfg;
}; };
export default (env: Record<string, string>): webpack.Configuration[] => { export default (
const isProd = env.mode === 'production'; env: Record<string, string>,
arv: Record<string, string>,
): webpack.Configuration[] => {
const isProd = arv.mode !== 'development';
return [getConfigByBrowser(isProd, 'chrome'), getConfigByBrowser(isProd, 'firefox')]; return [getConfigByBrowser(isProd, 'chrome'), getConfigByBrowser(isProd, 'firefox')];
}; };

View File

@@ -4,7 +4,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { join } from 'path'; import { join } from 'path';
import * as shelljs from 'shelljs'; import * as shelljs from 'shelljs';
const { load, exec, serial } = require('@xarc/run'); const { load, exec, serial, concurrent } = require('@xarc/run');
const makePublicFolder = () => { const makePublicFolder = () => {
if (!fs.existsSync('public/js')) { if (!fs.existsSync('public/js')) {
@@ -31,18 +31,21 @@ const copyFromNodeModules = () => {
}; };
load({ load({
build: [serial('postinstall', exec('gulp')), 'webpack'], build: [serial('postinstall', exec('gulp')), 'webpack'],
clean: exec('rimraf public coverage vendor'), clean: [exec('rimraf public coverage vendor'), 'clean:webpack'],
'clean:webpack': exec('rimraf dist'),
eslint: exec('eslint src . --fix'), eslint: exec('eslint src . --fix'),
less: exec('lessc assets/less/app.less public/css/app.css'), less: exec('lessc assets/less/app.less public/css/app.css'),
lint: ['prettier', 'eslint'], lint: ['prettier', 'eslint'],
postinstall: ['clean', makePublicFolder, copyFromNodeModules, 'less'], postinstall: ['clean', makePublicFolder, copyFromNodeModules, 'less'],
prettier: [exec('prettier --write .')], prettier: [exec('prettier --write .')],
'remotedev-server': exec('remotedev --hostname=localhost --port=8000'),
test: ['build', 'lint', 'test-jest', 'test-js'], test: ['build', 'lint', 'test-jest', 'test-js'],
'test-jest': [exec('jest --clearCache'), exec('jest --verbose --coverage')], 'test-jest': [exec('jest --clearCache'), exec('jest --verbose --coverage')],
'test-jest-update': exec('jest -u'), 'test-jest-update': exec('jest -u'),
'test-js': 'phantomjs tests/run.js', 'test-js': 'phantomjs tests/run.js',
watch: concurrent('watch-jest', 'webpack:watch', 'remotedev-server'),
'watch-jest': exec('jest --watch'), 'watch-jest': exec('jest --watch'),
webpack: [exec('webpack --mode production')], webpack: ['clean:webpack', exec('webpack --mode production')],
'webpack:dev': [exec('webpack --mode development')], 'webpack:dev': ['clean:webpack', exec('webpack --mode development')],
'webpack:watch': exec('webpack --mode development --watch'), 'webpack:watch': ['clean:webpack', exec('webpack --mode development --watch')],
}); });