mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-26 07:59:48 +03:00
Merge b315404534
into 6cdc18f2fa
This commit is contained in:
commit
4b699e2eba
|
@ -17,7 +17,7 @@ module.exports = {
|
||||||
'./demo/src/js/index',
|
'./demo/src/js/index',
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'demo/dist'),
|
path: path.join(__dirname, './dist'),
|
||||||
filename: 'js/bundle.js',
|
filename: 'js/bundle.js',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
@ -60,5 +60,5 @@ module.exports = {
|
||||||
},
|
},
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
},
|
},
|
||||||
devtool: 'eval-source-map',
|
devtool: isProduction ? 'sourcemap' : 'eval-source-map',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useGetPokemonByNameQuery } from '../rtk-query/pokemonApi';
|
||||||
|
import type { PokemonName } from '../rtk-query/pokemon.data';
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ label: 'Off', value: 0 },
|
||||||
|
{ label: '3s', value: 3000 },
|
||||||
|
{ label: '5s', value: 5000 },
|
||||||
|
{ label: '10s', value: 10000 },
|
||||||
|
{ label: '1m', value: 60000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Pokemon = ({ name }: { name: PokemonName }) => {
|
||||||
|
const [pollingInterval, setPollingInterval] = React.useState(60000);
|
||||||
|
|
||||||
|
const { data, error, isLoading, isFetching, refetch } =
|
||||||
|
useGetPokemonByNameQuery(name, {
|
||||||
|
pollingInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
float: 'left',
|
||||||
|
textAlign: 'center',
|
||||||
|
...(isFetching ? { background: '#e6ffe8' } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<>Oh no, there was an error loading {name}</>
|
||||||
|
) : isLoading ? (
|
||||||
|
<>Loading...</>
|
||||||
|
) : data ? (
|
||||||
|
<>
|
||||||
|
<h3>{data.species.name}</h3>
|
||||||
|
<div style={{ minWidth: 96, minHeight: 96 }}>
|
||||||
|
<img
|
||||||
|
src={data.sprites.front_shiny}
|
||||||
|
alt={data.species.name}
|
||||||
|
style={{ ...(isFetching ? { opacity: 0.3 } : {}) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block' }}>Polling interval</label>
|
||||||
|
<select
|
||||||
|
value={pollingInterval}
|
||||||
|
onChange={({ target: { value } }) =>
|
||||||
|
setPollingInterval(Number(value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intervalOptions.map(({ label, value }) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={refetch} disabled={isFetching}>
|
||||||
|
{isFetching ? 'Loading' : 'Manually refetch'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'No Data'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { PokemonName, POKEMON_NAMES } from '../rtk-query/pokemon.data';
|
||||||
|
import { Pokemon } from './Pokemon';
|
||||||
|
|
||||||
|
const getRandomPokemonName = () =>
|
||||||
|
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
|
||||||
|
|
||||||
|
export function PokemonView() {
|
||||||
|
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setPokemon((prev) => [...prev, getRandomPokemonName()])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add random pokemon
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}>
|
||||||
|
Add bulbasaur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pokemon.map((name, index) => (
|
||||||
|
<Pokemon key={index} name={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import pkg from '../../../package.json';
|
import pkg from '../../../../package.json';
|
||||||
import Button from 'react-bootstrap/Button';
|
import Button from 'react-bootstrap/Button';
|
||||||
import FormGroup from 'react-bootstrap/FormGroup';
|
import FormGroup from 'react-bootstrap/FormGroup';
|
||||||
import FormControl from 'react-bootstrap/FormControl';
|
import FormControl from 'react-bootstrap/FormControl';
|
||||||
|
@ -11,9 +11,10 @@ import InputGroup from 'react-bootstrap/InputGroup';
|
||||||
import Row from 'react-bootstrap/Row';
|
import Row from 'react-bootstrap/Row';
|
||||||
import * as base16 from 'base16';
|
import * as base16 from 'base16';
|
||||||
import { push as pushRoute } from 'connected-react-router';
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
import { PokemonView } from '../components/PokemonView';
|
||||||
import { Path } from 'history';
|
import { Path } from 'history';
|
||||||
import * as inspectorThemes from '../../../src/themes';
|
import * as inspectorThemes from '../../../../src/themes';
|
||||||
import getOptions, { Options } from './getOptions';
|
import getOptions, { Options } from '../getOptions';
|
||||||
import {
|
import {
|
||||||
AddFunctionAction,
|
AddFunctionAction,
|
||||||
AddHugeObjectAction,
|
AddHugeObjectAction,
|
||||||
|
@ -34,7 +35,7 @@ import {
|
||||||
ShuffleArrayAction,
|
ShuffleArrayAction,
|
||||||
TimeoutUpdateAction,
|
TimeoutUpdateAction,
|
||||||
ToggleTimeoutUpdateAction,
|
ToggleTimeoutUpdateAction,
|
||||||
} from './reducers';
|
} from '../reducers';
|
||||||
|
|
||||||
const styles: {
|
const styles: {
|
||||||
wrapper: CSSProperties;
|
wrapper: CSSProperties;
|
||||||
|
@ -56,9 +57,10 @@ const styles: {
|
||||||
header: {},
|
header: {},
|
||||||
content: {
|
content: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-evenly',
|
||||||
height: '50%',
|
padding: 8,
|
||||||
},
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -251,6 +253,7 @@ class DemoApp extends React.Component<Props> {
|
||||||
Shuffle Array
|
Shuffle Array
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<PokemonView />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.links}>
|
<div style={styles.links}>
|
||||||
<a onClick={this.toggleExtension} style={styles.link}>
|
<a onClick={this.toggleExtension} style={styles.link}>
|
|
@ -3,10 +3,10 @@ import { connect } from 'react-redux';
|
||||||
import { createDevTools } from '@redux-devtools/core';
|
import { createDevTools } from '@redux-devtools/core';
|
||||||
import DockMonitor from '@redux-devtools/dock-monitor';
|
import DockMonitor from '@redux-devtools/dock-monitor';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import DevtoolsInspector from '../../../src/DevtoolsInspector';
|
import DevtoolsInspector from '../../../../src/DevtoolsInspector';
|
||||||
import getOptions from './getOptions';
|
import getOptions from '../getOptions';
|
||||||
import { base16Themes } from '../../../src/utils/createStylingFromTheme';
|
import { base16Themes } from '../../../../src/utils/createStylingFromTheme';
|
||||||
import { DemoAppState } from './reducers';
|
import { DemoAppState } from '../reducers';
|
||||||
|
|
||||||
const CustomComponent = () => (
|
const CustomComponent = () => (
|
||||||
<div
|
<div
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { pokemonApi } from '../js/rtk-query/pokemonApi';
|
||||||
import {
|
import {
|
||||||
createStore,
|
|
||||||
applyMiddleware,
|
applyMiddleware,
|
||||||
compose,
|
compose,
|
||||||
StoreEnhancerStoreCreator,
|
StoreEnhancerStoreCreator,
|
||||||
|
@ -13,20 +14,17 @@ import { Route } from 'react-router';
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
|
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
|
||||||
import { persistState } from '@redux-devtools/core';
|
import { persistState } from '@redux-devtools/core';
|
||||||
import DemoApp from './DemoApp';
|
import DemoApp from './containers/DemoApp';
|
||||||
import createRootReducer from './reducers';
|
import createRootReducer from './reducers';
|
||||||
import getOptions from './getOptions';
|
import getOptions from './getOptions';
|
||||||
import { ConnectedDevTools, getDevTools } from './DevTools';
|
import { ConnectedDevTools, getDevTools } from './containers/DevTools';
|
||||||
|
|
||||||
function getDebugSessionKey() {
|
function getDebugSessionKey() {
|
||||||
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
|
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
|
||||||
return matches && matches.length > 0 ? matches[1] : null;
|
return matches && matches.length > 0 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROOT =
|
const ROOT = '/';
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? '/redux-devtools-inspector-monitor/'
|
|
||||||
: '/';
|
|
||||||
|
|
||||||
const DevTools = getDevTools(window.location);
|
const DevTools = getDevTools(window.location);
|
||||||
|
|
||||||
|
@ -51,7 +49,16 @@ const enhancer = compose(
|
||||||
persistState(getDebugSessionKey())
|
persistState(getDebugSessionKey())
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = createStore(createRootReducer(history), enhancer);
|
const store = configureStore({
|
||||||
|
reducer: createRootReducer(history),
|
||||||
|
devTools: false,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: false,
|
||||||
|
serializableCheck: false,
|
||||||
|
}).concat([pokemonApi.middleware]),
|
||||||
|
enhancers: [enhancer],
|
||||||
|
}); // createStore(createRootReducer(history), enhancer);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
RouterState,
|
RouterState,
|
||||||
} from 'connected-react-router';
|
} from 'connected-react-router';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
|
import { pokemonApi } from './rtk-query/pokemonApi';
|
||||||
|
|
||||||
type Nested = { long: { nested: { path: { to: { a: string } } }[] } };
|
type Nested = { long: { nested: { path: { to: { a: string } } }[] } };
|
||||||
|
|
||||||
|
@ -187,6 +188,7 @@ export interface DemoAppState {
|
||||||
addFunction: { f: (a: number, b: number, c: number) => number } | null;
|
addFunction: { f: (a: number, b: number, c: number) => number } | null;
|
||||||
addSymbol: { s: symbol; error: Error } | null;
|
addSymbol: { s: symbol; error: Error } | null;
|
||||||
shuffleArray: unknown[];
|
shuffleArray: unknown[];
|
||||||
|
[pokemonApi.reducerPath]: ReturnType<typeof pokemonApi.reducer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRootReducer = (
|
const createRootReducer = (
|
||||||
|
@ -259,6 +261,7 @@ const createRootReducer = (
|
||||||
: state,
|
: state,
|
||||||
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
|
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
|
||||||
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
|
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
|
||||||
|
[pokemonApi.reducerPath]: pokemonApi.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default createRootReducer;
|
export default createRootReducer;
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
export const POKEMON_NAMES = [
|
||||||
|
'bulbasaur',
|
||||||
|
'ivysaur',
|
||||||
|
'venusaur',
|
||||||
|
'charmander',
|
||||||
|
'charmeleon',
|
||||||
|
'charizard',
|
||||||
|
'squirtle',
|
||||||
|
'wartortle',
|
||||||
|
'blastoise',
|
||||||
|
'caterpie',
|
||||||
|
'metapod',
|
||||||
|
'butterfree',
|
||||||
|
'weedle',
|
||||||
|
'kakuna',
|
||||||
|
'beedrill',
|
||||||
|
'pidgey',
|
||||||
|
'pidgeotto',
|
||||||
|
'pidgeot',
|
||||||
|
'rattata',
|
||||||
|
'raticate',
|
||||||
|
'spearow',
|
||||||
|
'fearow',
|
||||||
|
'ekans',
|
||||||
|
'arbok',
|
||||||
|
'pikachu',
|
||||||
|
'raichu',
|
||||||
|
'sandshrew',
|
||||||
|
'sandslash',
|
||||||
|
'nidoran',
|
||||||
|
'nidorina',
|
||||||
|
'nidoqueen',
|
||||||
|
'nidoran',
|
||||||
|
'nidorino',
|
||||||
|
'nidoking',
|
||||||
|
'clefairy',
|
||||||
|
'clefable',
|
||||||
|
'vulpix',
|
||||||
|
'ninetales',
|
||||||
|
'jigglypuff',
|
||||||
|
'wigglytuff',
|
||||||
|
'zubat',
|
||||||
|
'golbat',
|
||||||
|
'oddish',
|
||||||
|
'gloom',
|
||||||
|
'vileplume',
|
||||||
|
'paras',
|
||||||
|
'parasect',
|
||||||
|
'venonat',
|
||||||
|
'venomoth',
|
||||||
|
'diglett',
|
||||||
|
'dugtrio',
|
||||||
|
'meowth',
|
||||||
|
'persian',
|
||||||
|
'psyduck',
|
||||||
|
'golduck',
|
||||||
|
'mankey',
|
||||||
|
'primeape',
|
||||||
|
'growlithe',
|
||||||
|
'arcanine',
|
||||||
|
'poliwag',
|
||||||
|
'poliwhirl',
|
||||||
|
'poliwrath',
|
||||||
|
'abra',
|
||||||
|
'kadabra',
|
||||||
|
'alakazam',
|
||||||
|
'machop',
|
||||||
|
'machoke',
|
||||||
|
'machamp',
|
||||||
|
'bellsprout',
|
||||||
|
'weepinbell',
|
||||||
|
'victreebel',
|
||||||
|
'tentacool',
|
||||||
|
'tentacruel',
|
||||||
|
'geodude',
|
||||||
|
'graveler',
|
||||||
|
'golem',
|
||||||
|
'ponyta',
|
||||||
|
'rapidash',
|
||||||
|
'slowpoke',
|
||||||
|
'slowbro',
|
||||||
|
'magnemite',
|
||||||
|
'magneton',
|
||||||
|
"farfetch'd",
|
||||||
|
'doduo',
|
||||||
|
'dodrio',
|
||||||
|
'seel',
|
||||||
|
'dewgong',
|
||||||
|
'grimer',
|
||||||
|
'muk',
|
||||||
|
'shellder',
|
||||||
|
'cloyster',
|
||||||
|
'gastly',
|
||||||
|
'haunter',
|
||||||
|
'gengar',
|
||||||
|
'onix',
|
||||||
|
'drowzee',
|
||||||
|
'hypno',
|
||||||
|
'krabby',
|
||||||
|
'kingler',
|
||||||
|
'voltorb',
|
||||||
|
'electrode',
|
||||||
|
'exeggcute',
|
||||||
|
'exeggutor',
|
||||||
|
'cubone',
|
||||||
|
'marowak',
|
||||||
|
'hitmonlee',
|
||||||
|
'hitmonchan',
|
||||||
|
'lickitung',
|
||||||
|
'koffing',
|
||||||
|
'weezing',
|
||||||
|
'rhyhorn',
|
||||||
|
'rhydon',
|
||||||
|
'chansey',
|
||||||
|
'tangela',
|
||||||
|
'kangaskhan',
|
||||||
|
'horsea',
|
||||||
|
'seadra',
|
||||||
|
'goldeen',
|
||||||
|
'seaking',
|
||||||
|
'staryu',
|
||||||
|
'starmie',
|
||||||
|
'mr. mime',
|
||||||
|
'scyther',
|
||||||
|
'jynx',
|
||||||
|
'electabuzz',
|
||||||
|
'magmar',
|
||||||
|
'pinsir',
|
||||||
|
'tauros',
|
||||||
|
'magikarp',
|
||||||
|
'gyarados',
|
||||||
|
'lapras',
|
||||||
|
'ditto',
|
||||||
|
'eevee',
|
||||||
|
'vaporeon',
|
||||||
|
'jolteon',
|
||||||
|
'flareon',
|
||||||
|
'porygon',
|
||||||
|
'omanyte',
|
||||||
|
'omastar',
|
||||||
|
'kabuto',
|
||||||
|
'kabutops',
|
||||||
|
'aerodactyl',
|
||||||
|
'snorlax',
|
||||||
|
'articuno',
|
||||||
|
'zapdos',
|
||||||
|
'moltres',
|
||||||
|
'dratini',
|
||||||
|
'dragonair',
|
||||||
|
'dragonite',
|
||||||
|
'mewtwo',
|
||||||
|
'mew',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type PokemonName = typeof POKEMON_NAMES[number];
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
import type { PokemonName } from './pokemon.data';
|
||||||
|
|
||||||
|
export const pokemonApi = createApi({
|
||||||
|
reducerPath: 'pokemonApi',
|
||||||
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
|
||||||
|
tagTypes: ['pokemon'],
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getPokemonByName: builder.query({
|
||||||
|
query: (name: PokemonName) => `pokemon/${name}`,
|
||||||
|
providesTags: (result, error, name: PokemonName) => [
|
||||||
|
{ type: 'pokemon' },
|
||||||
|
{ type: 'pokemon', id: name },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export hooks for usage in functional components
|
||||||
|
export const { useGetPokemonByNameQuery } = pokemonApi;
|
|
@ -24,7 +24,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --config demo/config/webpack.config.ts",
|
"start": "webpack-dev-server --config demo/config/webpack.config.ts",
|
||||||
"stats": "webpack --profile --json > stats.json",
|
"stats": "webpack --profile --json > stats.json",
|
||||||
"build:demo": "NODE_ENV=production webpack -p",
|
"build:demo": "cross-env NODE_ENV=production webpack -p --config demo/config/webpack.config.ts",
|
||||||
"build": "npm run build:types && npm run build:js",
|
"build": "npm run build:types && npm run build:js",
|
||||||
"build:types": "tsc --emitDeclarationOnly",
|
"build:types": "tsc --emitDeclarationOnly",
|
||||||
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
|
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
|
||||||
|
@ -51,11 +51,13 @@
|
||||||
"react-base16-styling": "^0.8.0",
|
"react-base16-styling": "^0.8.0",
|
||||||
"react-dragula": "^1.1.17",
|
"react-dragula": "^1.1.17",
|
||||||
"react-json-tree": "^0.15.0",
|
"react-json-tree": "^0.15.0",
|
||||||
"redux-devtools-themes": "^1.0.0"
|
"redux-devtools-themes": "^1.0.0",
|
||||||
|
"reselect": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redux-devtools/core": "^3.9.0",
|
"@redux-devtools/core": "^3.9.0",
|
||||||
"@redux-devtools/dock-monitor": "^1.4.0",
|
"@redux-devtools/dock-monitor": "^1.4.0",
|
||||||
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
"@types/dateformat": "^3.0.1",
|
"@types/dateformat": "^3.0.1",
|
||||||
"@types/hex-rgba": "^1.0.1",
|
"@types/hex-rgba": "^1.0.1",
|
||||||
"@types/history": "^4.7.9",
|
"@types/history": "^4.7.9",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { PerformAction } from '@redux-devtools/core';
|
||||||
import { StylingFunction } from 'react-base16-styling';
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
import ActionListRow from './ActionListRow';
|
import ActionListRow from './ActionListRow';
|
||||||
import ActionListHeader from './ActionListHeader';
|
import ActionListHeader from './ActionListHeader';
|
||||||
|
import { ActionForm } from './redux';
|
||||||
|
|
||||||
function getTimestamps<A extends Action<unknown>>(
|
function getTimestamps<A extends Action<unknown>>(
|
||||||
actions: { [actionId: number]: PerformAction<A> },
|
actions: { [actionId: number]: PerformAction<A> },
|
||||||
|
@ -21,15 +22,16 @@ function getTimestamps<A extends Action<unknown>>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<A extends Action<unknown>> {
|
interface Props<S, A extends Action<unknown>> {
|
||||||
actions: { [actionId: number]: PerformAction<A> };
|
actions: { [actionId: number]: PerformAction<A> };
|
||||||
actionIds: number[];
|
actionIds: number[];
|
||||||
isWideLayout: boolean;
|
isWideLayout: boolean;
|
||||||
searchValue: string | undefined;
|
|
||||||
selectedActionId: number | null;
|
selectedActionId: number | null;
|
||||||
startActionId: number | null;
|
startActionId: number | null;
|
||||||
skippedActionIds: number[];
|
skippedActionIds: number[];
|
||||||
draggableActions: boolean;
|
draggableActions: boolean;
|
||||||
|
actionForm: ActionForm;
|
||||||
|
filteredActionIds: number[];
|
||||||
hideMainButtons: boolean | undefined;
|
hideMainButtons: boolean | undefined;
|
||||||
hideActionButtons: boolean | undefined;
|
hideActionButtons: boolean | undefined;
|
||||||
styling: StylingFunction;
|
styling: StylingFunction;
|
||||||
|
@ -40,18 +42,20 @@ interface Props<A extends Action<unknown>> {
|
||||||
onCommit: () => void;
|
onCommit: () => void;
|
||||||
onSweep: () => void;
|
onSweep: () => void;
|
||||||
onReorderAction: (actionId: number, beforeActionId: number) => void;
|
onReorderAction: (actionId: number, beforeActionId: number) => void;
|
||||||
|
onActionFormChange: (formValues: Partial<ActionForm>) => void;
|
||||||
currentActionId: number;
|
currentActionId: number;
|
||||||
lastActionId: number;
|
lastActionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ActionList<
|
export default class ActionList<
|
||||||
|
S,
|
||||||
A extends Action<unknown>
|
A extends Action<unknown>
|
||||||
> extends PureComponent<Props<A>> {
|
> extends PureComponent<Props<S, A>> {
|
||||||
node?: HTMLDivElement | null;
|
node?: HTMLDivElement | null;
|
||||||
scrollDown?: boolean;
|
scrollDown?: boolean;
|
||||||
drake?: Drake;
|
drake?: Drake;
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps: Props<A>) {
|
UNSAFE_componentWillReceiveProps(nextProps: Props<S, A>) {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
if (!node) {
|
if (!node) {
|
||||||
this.scrollDown = true;
|
this.scrollDown = true;
|
||||||
|
@ -119,24 +123,16 @@ export default class ActionList<
|
||||||
startActionId,
|
startActionId,
|
||||||
onSelect,
|
onSelect,
|
||||||
onSearch,
|
onSearch,
|
||||||
searchValue,
|
|
||||||
currentActionId,
|
currentActionId,
|
||||||
hideMainButtons,
|
hideMainButtons,
|
||||||
hideActionButtons,
|
hideActionButtons,
|
||||||
onCommit,
|
onCommit,
|
||||||
onSweep,
|
onSweep,
|
||||||
|
onActionFormChange,
|
||||||
onJumpToState,
|
onJumpToState,
|
||||||
|
actionForm,
|
||||||
|
filteredActionIds,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
|
||||||
const filteredActionIds = searchValue
|
|
||||||
? actionIds.filter(
|
|
||||||
(id) =>
|
|
||||||
(actions[id].action.type as string)
|
|
||||||
.toLowerCase()
|
|
||||||
.indexOf(lowerSearchValue as string) !== -1
|
|
||||||
)
|
|
||||||
: actionIds;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="actionList"
|
key="actionList"
|
||||||
|
@ -150,12 +146,14 @@ export default class ActionList<
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onCommit={onCommit}
|
onCommit={onCommit}
|
||||||
onSweep={onSweep}
|
onSweep={onSweep}
|
||||||
|
actionForm={actionForm}
|
||||||
|
onActionFormChange={onActionFormChange}
|
||||||
hideMainButtons={hideMainButtons}
|
hideMainButtons={hideMainButtons}
|
||||||
hasSkippedActions={skippedActionIds.length > 0}
|
hasSkippedActions={skippedActionIds.length > 0}
|
||||||
hasStagedActions={actionIds.length > 1}
|
hasStagedActions={actionIds.length > 1}
|
||||||
/>
|
/>
|
||||||
<div {...styling('actionListRows')} ref={this.getRef}>
|
<div {...styling('actionListRows')} ref={this.getRef}>
|
||||||
{filteredActionIds.map((actionId) => (
|
{filteredActionIds.map((actionId: number) => (
|
||||||
<ActionListRow
|
<ActionListRow
|
||||||
key={actionId}
|
key={actionId}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
|
|
|
@ -2,12 +2,27 @@ import React, { FunctionComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StylingFunction } from 'react-base16-styling';
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
import RightSlider from './RightSlider';
|
import RightSlider from './RightSlider';
|
||||||
|
import { ActionForm } from './redux';
|
||||||
|
|
||||||
const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] =>
|
const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] =>
|
||||||
[hasSkippedActions && 'Sweep', 'Commit'].filter(
|
[hasSkippedActions && 'Sweep', 'Commit'].filter(
|
||||||
(a): a is 'Sweep' | 'Commit' => !!a
|
(a): a is 'Sweep' | 'Commit' => !!a
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleButton = {
|
||||||
|
label: {
|
||||||
|
rtkq(val: boolean) {
|
||||||
|
return val ? 'Show rtk-query actions' : 'Hide rtk-query actions';
|
||||||
|
},
|
||||||
|
noop(val: boolean) {
|
||||||
|
return val ? 'Show noop actions' : 'Hide noop actions';
|
||||||
|
},
|
||||||
|
invertSearch(val: boolean) {
|
||||||
|
return val ? 'Disable inverse search' : 'Activate inverse search';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
styling: StylingFunction;
|
styling: StylingFunction;
|
||||||
onSearch: (value: string) => void;
|
onSearch: (value: string) => void;
|
||||||
|
@ -16,6 +31,8 @@ interface Props {
|
||||||
hideMainButtons: boolean | undefined;
|
hideMainButtons: boolean | undefined;
|
||||||
hasSkippedActions: boolean;
|
hasSkippedActions: boolean;
|
||||||
hasStagedActions: boolean;
|
hasStagedActions: boolean;
|
||||||
|
actionForm: ActionForm;
|
||||||
|
onActionFormChange: (formValues: Partial<ActionForm>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionListHeader: FunctionComponent<Props> = ({
|
const ActionListHeader: FunctionComponent<Props> = ({
|
||||||
|
@ -26,41 +43,94 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
||||||
onCommit,
|
onCommit,
|
||||||
onSweep,
|
onSweep,
|
||||||
hideMainButtons,
|
hideMainButtons,
|
||||||
}) => (
|
actionForm,
|
||||||
<div {...styling('actionListHeader')}>
|
onActionFormChange,
|
||||||
<input
|
}) => {
|
||||||
{...styling('actionListHeaderSearch')}
|
const { isNoopFilterActive, isRtkQueryFilterActive, isInvertSearchActive } =
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
actionForm;
|
||||||
placeholder="filter..."
|
|
||||||
/>
|
return (
|
||||||
{!hideMainButtons && (
|
<>
|
||||||
<div {...styling('actionListHeaderWrapper')}>
|
<div {...styling('actionListHeader')}>
|
||||||
<RightSlider shown={hasStagedActions} styling={styling}>
|
<input
|
||||||
<div {...styling('actionListHeaderSelector')}>
|
{...styling('actionListHeaderSearch')}
|
||||||
{getActiveButtons(hasSkippedActions).map((btn) => (
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
<div
|
placeholder="filter..."
|
||||||
key={btn}
|
/>
|
||||||
onClick={() =>
|
<div {...styling('toggleButtonWrapper')}>
|
||||||
({
|
<button
|
||||||
Commit: onCommit,
|
title={toggleButton.label.noop(isNoopFilterActive)}
|
||||||
Sweep: onSweep,
|
aria-label={toggleButton.label.noop(isNoopFilterActive)}
|
||||||
}[btn]())
|
aria-pressed={isNoopFilterActive}
|
||||||
}
|
onClick={() =>
|
||||||
{...styling(
|
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
|
||||||
['selectorButton', 'selectorButtonSmall'],
|
}
|
||||||
false,
|
type="button"
|
||||||
true
|
{...styling('toggleButton')}
|
||||||
)}
|
>
|
||||||
>
|
noop
|
||||||
{btn}
|
</button>
|
||||||
</div>
|
<button
|
||||||
))}
|
title={toggleButton.label.rtkq(isRtkQueryFilterActive)}
|
||||||
</div>
|
aria-label={toggleButton.label.rtkq(isRtkQueryFilterActive)}
|
||||||
</RightSlider>
|
aria-pressed={isRtkQueryFilterActive}
|
||||||
|
onClick={() =>
|
||||||
|
onActionFormChange({
|
||||||
|
isRtkQueryFilterActive: !isRtkQueryFilterActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
{...styling('toggleButton')}
|
||||||
|
>
|
||||||
|
RTKQ
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title={toggleButton.label.invertSearch(isInvertSearchActive)}
|
||||||
|
aria-label={toggleButton.label.invertSearch(isInvertSearchActive)}
|
||||||
|
aria-pressed={isInvertSearchActive}
|
||||||
|
onClick={() =>
|
||||||
|
onActionFormChange({
|
||||||
|
isInvertSearchActive: !isInvertSearchActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
{...styling('toggleButton')}
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{!hideMainButtons && (
|
||||||
</div>
|
<div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
|
||||||
);
|
<div {...styling('actionListHeaderWrapper')}>
|
||||||
|
<RightSlider shown={hasStagedActions} styling={styling}>
|
||||||
|
<div {...styling('actionListHeaderSelector')}>
|
||||||
|
{getActiveButtons(hasSkippedActions).map((btn) => (
|
||||||
|
<div
|
||||||
|
key={btn}
|
||||||
|
onClick={() =>
|
||||||
|
({
|
||||||
|
Commit: onCommit,
|
||||||
|
Sweep: onSweep,
|
||||||
|
}[btn]())
|
||||||
|
}
|
||||||
|
{...styling(
|
||||||
|
['selectorButton', 'selectorButtonSmall'],
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{btn}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</RightSlider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ActionListHeader.propTypes = {
|
ActionListHeader.propTypes = {
|
||||||
styling: PropTypes.func.isRequired,
|
styling: PropTypes.func.isRequired,
|
||||||
|
@ -70,6 +140,13 @@ ActionListHeader.propTypes = {
|
||||||
hideMainButtons: PropTypes.bool,
|
hideMainButtons: PropTypes.bool,
|
||||||
hasSkippedActions: PropTypes.bool.isRequired,
|
hasSkippedActions: PropTypes.bool.isRequired,
|
||||||
hasStagedActions: PropTypes.bool.isRequired,
|
hasStagedActions: PropTypes.bool.isRequired,
|
||||||
|
actionForm: PropTypes.shape({
|
||||||
|
searchValue: PropTypes.string.isRequired,
|
||||||
|
isNoopFilterActive: PropTypes.bool.isRequired,
|
||||||
|
isRtkQueryFilterActive: PropTypes.bool.isRequired,
|
||||||
|
isInvertSearchActive: PropTypes.bool.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
onActionFormChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActionListHeader;
|
export default ActionListHeader;
|
||||||
|
|
|
@ -21,12 +21,17 @@ import ActionList from './ActionList';
|
||||||
import ActionPreview, { Tab } from './ActionPreview';
|
import ActionPreview, { Tab } from './ActionPreview';
|
||||||
import getInspectedState from './utils/getInspectedState';
|
import getInspectedState from './utils/getInspectedState';
|
||||||
import createDiffPatcher from './createDiffPatcher';
|
import createDiffPatcher from './createDiffPatcher';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
import {
|
import {
|
||||||
|
ActionForm,
|
||||||
|
changeActionFormValues,
|
||||||
DevtoolsInspectorAction,
|
DevtoolsInspectorAction,
|
||||||
DevtoolsInspectorState,
|
DevtoolsInspectorState,
|
||||||
reducer,
|
reducer,
|
||||||
updateMonitorState,
|
updateMonitorState,
|
||||||
} from './redux';
|
} from './redux';
|
||||||
|
import { makeSelectFilteredActions } from './utils/filters';
|
||||||
|
import { computeSelectorSource } from './utils/selectors';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const {
|
const {
|
||||||
|
@ -233,6 +238,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
updateSizeTimeout?: number;
|
updateSizeTimeout?: number;
|
||||||
inspectorRef?: HTMLDivElement | null;
|
inspectorRef?: HTMLDivElement | null;
|
||||||
|
|
||||||
|
selectorsSource = computeSelectorSource(this.props);
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateSizeMode();
|
this.updateSizeMode();
|
||||||
this.updateSizeTimeout = window.setInterval(
|
this.updateSizeTimeout = window.setInterval(
|
||||||
|
@ -249,6 +256,10 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.props.dispatch(updateMonitorState(monitorState));
|
this.props.dispatch(updateMonitorState(monitorState));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleActionFormChange = (formValues: Partial<ActionForm>) => {
|
||||||
|
this.props.dispatch(changeActionFormValues(formValues));
|
||||||
|
};
|
||||||
|
|
||||||
updateSizeMode() {
|
updateSizeMode() {
|
||||||
const isWideLayout = this.inspectorRef!.offsetWidth > 500;
|
const isWideLayout = this.inspectorRef!.offsetWidth > 500;
|
||||||
|
|
||||||
|
@ -286,6 +297,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.inspectorRef = node;
|
this.inspectorRef = node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectFilteredActions = makeSelectFilteredActions();
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
stagedActionIds: actionIds,
|
stagedActionIds: actionIds,
|
||||||
|
@ -301,7 +314,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
hideMainButtons,
|
hideMainButtons,
|
||||||
hideActionButtons,
|
hideActionButtons,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { selectedActionId, startActionId, searchValue, tabName } =
|
const { selectedActionId, startActionId, actionForm, tabName } =
|
||||||
monitorState;
|
monitorState;
|
||||||
const inspectedPathType =
|
const inspectedPathType =
|
||||||
tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath';
|
tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath';
|
||||||
|
@ -309,6 +322,12 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.state;
|
this.state;
|
||||||
const { base16Theme, styling } = themeState;
|
const { base16Theme, styling } = themeState;
|
||||||
|
|
||||||
|
this.selectorsSource = computeSelectorSource(
|
||||||
|
this.props,
|
||||||
|
this.selectorsSource
|
||||||
|
);
|
||||||
|
const filteredActionIds = this.selectFilteredActions(this.selectorsSource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="inspector"
|
key="inspector"
|
||||||
|
@ -323,7 +342,6 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
actions,
|
actions,
|
||||||
actionIds,
|
actionIds,
|
||||||
isWideLayout,
|
isWideLayout,
|
||||||
searchValue,
|
|
||||||
selectedActionId,
|
selectedActionId,
|
||||||
startActionId,
|
startActionId,
|
||||||
skippedActionIds,
|
skippedActionIds,
|
||||||
|
@ -332,7 +350,10 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
hideActionButtons,
|
hideActionButtons,
|
||||||
styling,
|
styling,
|
||||||
}}
|
}}
|
||||||
|
actionForm={actionForm}
|
||||||
|
filteredActionIds={filteredActionIds}
|
||||||
onSearch={this.handleSearch}
|
onSearch={this.handleSearch}
|
||||||
|
onActionFormChange={this.handleActionFormChange}
|
||||||
onSelect={this.handleSelectAction}
|
onSelect={this.handleSelectAction}
|
||||||
onToggleAction={this.handleToggleAction}
|
onToggleAction={this.handleToggleAction}
|
||||||
onJumpToState={this.handleJumpToState}
|
onJumpToState={this.handleJumpToState}
|
||||||
|
@ -399,9 +420,9 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.props.dispatch(sweep());
|
this.props.dispatch(sweep());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSearch = (val: string) => {
|
handleSearch = debounce((val: string) => {
|
||||||
this.updateMonitorState({ searchValue: val });
|
this.handleActionFormChange({ searchValue: val });
|
||||||
};
|
}, 200);
|
||||||
|
|
||||||
handleSelectAction = (
|
handleSelectAction = (
|
||||||
e: React.MouseEvent<HTMLDivElement>,
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
|
|
|
@ -4,17 +4,40 @@ import { DevtoolsInspectorProps } from './DevtoolsInspector';
|
||||||
const UPDATE_MONITOR_STATE =
|
const UPDATE_MONITOR_STATE =
|
||||||
'@@redux-devtools-inspector-monitor/UPDATE_MONITOR_STATE';
|
'@@redux-devtools-inspector-monitor/UPDATE_MONITOR_STATE';
|
||||||
|
|
||||||
|
const ACTION_FORM_VALUE_CHANGE =
|
||||||
|
'@@redux-devtools-inspector-monitor/ACTION_FORM_VALUE_CHANGE';
|
||||||
|
|
||||||
|
export interface ActionForm {
|
||||||
|
readonly searchValue: string;
|
||||||
|
readonly isNoopFilterActive: boolean;
|
||||||
|
readonly isRtkQueryFilterActive: boolean;
|
||||||
|
readonly isInvertSearchActive: boolean;
|
||||||
|
}
|
||||||
export interface UpdateMonitorStateAction {
|
export interface UpdateMonitorStateAction {
|
||||||
type: typeof UPDATE_MONITOR_STATE;
|
type: typeof UPDATE_MONITOR_STATE;
|
||||||
monitorState: Partial<DevtoolsInspectorState>;
|
monitorState: Partial<DevtoolsInspectorState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChangeActionFormAction {
|
||||||
|
type: typeof ACTION_FORM_VALUE_CHANGE;
|
||||||
|
formValues: Partial<ActionForm>;
|
||||||
|
}
|
||||||
|
|
||||||
export function updateMonitorState(
|
export function updateMonitorState(
|
||||||
monitorState: Partial<DevtoolsInspectorState>
|
monitorState: Partial<DevtoolsInspectorState>
|
||||||
): UpdateMonitorStateAction {
|
): UpdateMonitorStateAction {
|
||||||
return { type: UPDATE_MONITOR_STATE, monitorState };
|
return { type: UPDATE_MONITOR_STATE, monitorState };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DevtoolsInspectorAction = UpdateMonitorStateAction;
|
export function changeActionFormValues(
|
||||||
|
formValues: Partial<ActionForm>
|
||||||
|
): ChangeActionFormAction {
|
||||||
|
return { type: ACTION_FORM_VALUE_CHANGE, formValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DevtoolsInspectorAction =
|
||||||
|
| UpdateMonitorStateAction
|
||||||
|
| ChangeActionFormAction;
|
||||||
|
|
||||||
export interface DevtoolsInspectorState {
|
export interface DevtoolsInspectorState {
|
||||||
selectedActionId: number | null;
|
selectedActionId: number | null;
|
||||||
|
@ -22,7 +45,7 @@ export interface DevtoolsInspectorState {
|
||||||
inspectedActionPath: (string | number)[];
|
inspectedActionPath: (string | number)[];
|
||||||
inspectedStatePath: (string | number)[];
|
inspectedStatePath: (string | number)[];
|
||||||
tabName: string;
|
tabName: string;
|
||||||
searchValue?: string;
|
actionForm: ActionForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_STATE: DevtoolsInspectorState = {
|
export const DEFAULT_STATE: DevtoolsInspectorState = {
|
||||||
|
@ -31,18 +54,35 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
|
||||||
inspectedActionPath: [],
|
inspectedActionPath: [],
|
||||||
inspectedStatePath: [],
|
inspectedStatePath: [],
|
||||||
tabName: 'Diff',
|
tabName: 'Diff',
|
||||||
|
actionForm: {
|
||||||
|
searchValue: '',
|
||||||
|
isNoopFilterActive: false,
|
||||||
|
isRtkQueryFilterActive: false,
|
||||||
|
isInvertSearchActive: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function reduceUpdateState(
|
function internalMonitorActionReducer(
|
||||||
state: DevtoolsInspectorState,
|
state: DevtoolsInspectorState,
|
||||||
action: DevtoolsInspectorAction
|
action: DevtoolsInspectorAction
|
||||||
) {
|
): DevtoolsInspectorState {
|
||||||
return action.type === UPDATE_MONITOR_STATE
|
switch (action.type) {
|
||||||
? {
|
case UPDATE_MONITOR_STATE:
|
||||||
|
return {
|
||||||
...state,
|
...state,
|
||||||
...action.monitorState,
|
...action.monitorState,
|
||||||
}
|
};
|
||||||
: state;
|
case ACTION_FORM_VALUE_CHANGE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
actionForm: {
|
||||||
|
...state.actionForm,
|
||||||
|
...action.formValues,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducer<S, A extends Action<unknown>>(
|
export function reducer<S, A extends Action<unknown>>(
|
||||||
|
@ -51,6 +91,6 @@ export function reducer<S, A extends Action<unknown>>(
|
||||||
action: DevtoolsInspectorAction
|
action: DevtoolsInspectorAction
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...reduceUpdateState(state, action),
|
...internalMonitorActionReducer(state, action),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import jss, { Styles, StyleSheet } from 'jss';
|
import jss, { StyleSheet } from 'jss';
|
||||||
import preset from 'jss-preset-default';
|
import preset from 'jss-preset-default';
|
||||||
import { createStyling } from 'react-base16-styling';
|
import { createStyling } from 'react-base16-styling';
|
||||||
import rgba from 'hex-rgba';
|
import rgba from 'hex-rgba';
|
||||||
|
@ -33,6 +33,8 @@ const colorMap = (theme: Base16Theme) => ({
|
||||||
LINK_COLOR: rgba(theme.base0E, 90),
|
LINK_COLOR: rgba(theme.base0E, 90),
|
||||||
LINK_HOVER_COLOR: theme.base0E,
|
LINK_HOVER_COLOR: theme.base0E,
|
||||||
ERROR_COLOR: theme.base08,
|
ERROR_COLOR: theme.base08,
|
||||||
|
TOGGLE_BUTTON_BACKGROUND: rgba(theme.base00, 70),
|
||||||
|
TOGGLE_BUTTON_SELECTED_BACKGROUND: theme.base04,
|
||||||
});
|
});
|
||||||
|
|
||||||
type Color = keyof ReturnType<typeof colorMap>;
|
type Color = keyof ReturnType<typeof colorMap>;
|
||||||
|
@ -83,6 +85,11 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
'border-color': map.LIST_BORDER_COLOR,
|
'border-color': map.LIST_BORDER_COLOR,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
actionListHeaderSecondRow: {
|
||||||
|
padding: '5px 10px',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
|
||||||
actionListRows: {
|
actionListRows: {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
|
||||||
|
@ -106,7 +113,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
|
|
||||||
actionListHeaderSelector: {
|
actionListHeaderSelector: {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
'margin-right': '10px',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actionListWide: {
|
actionListWide: {
|
||||||
|
@ -330,6 +336,55 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleButtonWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
height: 20,
|
||||||
|
margin: 0,
|
||||||
|
padding: '0 10px 0 0',
|
||||||
|
'& > *': {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleButton: {
|
||||||
|
color: 'inherit',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0px 4px',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
letterSpacing: '-0.7px',
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
fontWeight: '700',
|
||||||
|
'border-style': 'solid',
|
||||||
|
'border-width': '1px',
|
||||||
|
'border-left-width': 0,
|
||||||
|
|
||||||
|
'&:first-child': {
|
||||||
|
'border-left-width': '1px',
|
||||||
|
'border-top-left-radius': '3px',
|
||||||
|
'border-bottom-left-radius': '3px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:last-child': {
|
||||||
|
'border-top-right-radius': '3px',
|
||||||
|
'border-bottom-right-radius': '3px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
'background-color': map.TOGGLE_BUTTON_BACKGROUND,
|
||||||
|
|
||||||
|
'border-color': map.TAB_BORDER_COLOR,
|
||||||
|
|
||||||
|
'&[aria-pressed="true"]': {
|
||||||
|
color: map.BACKGROUND_COLOR,
|
||||||
|
backgroundColor: map.TOGGLE_BUTTON_SELECTED_BACKGROUND,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
diff: {
|
diff: {
|
||||||
padding: '2px 3px',
|
padding: '2px 3px',
|
||||||
'border-radius': '3px',
|
'border-radius': '3px',
|
||||||
|
|
152
packages/redux-devtools-inspector-monitor/src/utils/filters.ts
Normal file
152
packages/redux-devtools-inspector-monitor/src/utils/filters.ts
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import type { Action } from 'redux';
|
||||||
|
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
||||||
|
import { ActionForm } from '../redux';
|
||||||
|
import { makeSelectRtkQueryActionRegex } from './rtk-query';
|
||||||
|
import { createShallowEqualSelector, SelectorsSource } from './selectors';
|
||||||
|
|
||||||
|
type ComputedStates<S> = LiftedState<
|
||||||
|
S,
|
||||||
|
Action<unknown>,
|
||||||
|
unknown
|
||||||
|
>['computedStates'];
|
||||||
|
|
||||||
|
function isNoopAction<S>(
|
||||||
|
actionId: number,
|
||||||
|
computedStates: ComputedStates<S>
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
actionId === 0 ||
|
||||||
|
computedStates[actionId]?.state === computedStates[actionId - 1]?.state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStateChangingAction<S>(
|
||||||
|
actionIds: number[],
|
||||||
|
computedStates: ComputedStates<S>
|
||||||
|
): number[] {
|
||||||
|
return actionIds.filter(
|
||||||
|
(actionId) => !isNoopAction(actionId, computedStates)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterActionsBySearchValue<A extends Action<unknown>>(
|
||||||
|
searchValue: string | undefined,
|
||||||
|
actionIds: number[],
|
||||||
|
actions: Record<number, PerformAction<A>>
|
||||||
|
): number[] {
|
||||||
|
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
||||||
|
|
||||||
|
if (!lowerSearchValue || !actionIds.length) {
|
||||||
|
return actionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionIds.filter((id) => {
|
||||||
|
const type = actions[id].action.type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
type != null &&
|
||||||
|
`${type as string}`.toLowerCase().includes(lowerSearchValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterOutRtkQueryActions(
|
||||||
|
actionIds: number[],
|
||||||
|
actions: Record<number, PerformAction<Action<unknown>>>,
|
||||||
|
rtkQueryRegex: RegExp | null
|
||||||
|
) {
|
||||||
|
if (!rtkQueryRegex || actionIds.length === 0) {
|
||||||
|
return actionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionIds.filter((actionId) => {
|
||||||
|
const type = actions[actionId].action.type;
|
||||||
|
|
||||||
|
return typeof type !== 'string' || !rtkQueryRegex.test(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invertSearchResults(
|
||||||
|
actionIds: number[],
|
||||||
|
filteredActionIds: number[]
|
||||||
|
): number[] {
|
||||||
|
if (
|
||||||
|
actionIds.length === 0 ||
|
||||||
|
actionIds.length === filteredActionIds.length ||
|
||||||
|
filteredActionIds.length === 0
|
||||||
|
) {
|
||||||
|
return actionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredSet = new Set(filteredActionIds);
|
||||||
|
|
||||||
|
return actionIds.filter((actionId) => !filteredSet.has(actionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterActionsPayload<S, A extends Action<unknown>> {
|
||||||
|
readonly actionIds: number[];
|
||||||
|
readonly actions: Record<number, PerformAction<A>>;
|
||||||
|
readonly computedStates: ComputedStates<S>;
|
||||||
|
readonly actionForm: ActionForm;
|
||||||
|
readonly rtkQueryRegex: RegExp | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterActions<S, A extends Action<unknown>>({
|
||||||
|
actionIds,
|
||||||
|
actions,
|
||||||
|
computedStates,
|
||||||
|
actionForm,
|
||||||
|
rtkQueryRegex,
|
||||||
|
}: FilterActionsPayload<S, A>): number[] {
|
||||||
|
let output = filterActionsBySearchValue(
|
||||||
|
actionForm.searchValue,
|
||||||
|
actionIds,
|
||||||
|
actions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (actionForm.isNoopFilterActive) {
|
||||||
|
output = filterStateChangingAction(output, computedStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionForm.isRtkQueryFilterActive && rtkQueryRegex) {
|
||||||
|
output = filterOutRtkQueryActions(output, actions, rtkQueryRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionForm.isInvertSearchActive) {
|
||||||
|
output = invertSearchResults(actionIds, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectFilteredActions<S, A extends Action<unknown>> {
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a selector that given `SelectorsSource` returns
|
||||||
|
* a list of filtered `actionsIds`.
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
export function makeSelectFilteredActions<
|
||||||
|
S,
|
||||||
|
A extends Action<unknown>
|
||||||
|
>(): SelectFilteredActions<S, A> {
|
||||||
|
const selectRegex = makeSelectRtkQueryActionRegex();
|
||||||
|
|
||||||
|
return createShallowEqualSelector(
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): FilterActionsPayload<S, A> => {
|
||||||
|
const actionForm = selectorsSource.monitorState.actionForm;
|
||||||
|
const { actionIds, actions, computedStates } = selectorsSource;
|
||||||
|
|
||||||
|
return {
|
||||||
|
actionIds,
|
||||||
|
actions,
|
||||||
|
computedStates,
|
||||||
|
actionForm,
|
||||||
|
rtkQueryRegex: selectRegex(selectorsSource),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
filterActions
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Borrowed from `react-redux`.
|
||||||
|
* @param {unknown} obj The object to inspect.
|
||||||
|
* @returns {boolean} True if the argument appears to be a plain object.
|
||||||
|
* @see https://github.com/reduxjs/react-redux/blob/2c7ef25a0704efcf10e41112d88ae9867e946d10/src/utils/isPlainObject.js
|
||||||
|
*/
|
||||||
|
export function isPlainObject(obj: unknown): obj is Record<string, unknown> {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proto = Object.getPrototypeOf(obj);
|
||||||
|
|
||||||
|
if (proto === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseProto = proto;
|
||||||
|
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||||
|
baseProto = Object.getPrototypeOf(baseProto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto === baseProto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function identity<T>(val: T): T {
|
||||||
|
return val;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// https://stackoverflow.com/a/9310752
|
||||||
|
export function escapeRegExpSpecialCharacter(text: string): string {
|
||||||
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ```ts
|
||||||
|
* const entries = ['a', 'b', 'c', 'd.'];
|
||||||
|
*
|
||||||
|
* oneOfGroup(entries) // returns "(a|b|c|d\\.)"
|
||||||
|
* ```
|
||||||
|
* @param onOf
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function oneOfGroup(onOf: string[]): string {
|
||||||
|
return `(${onOf.map(escapeRegExpSpecialCharacter).join('|')})`;
|
||||||
|
}
|
107
packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
Normal file
107
packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { isPlainObject } from './object';
|
||||||
|
import { oneOfGroup } from './regexp';
|
||||||
|
import { createShallowEqualSelector, SelectorsSource } from './selectors';
|
||||||
|
|
||||||
|
interface RtkQueryApiState {
|
||||||
|
queries: Record<string, unknown>;
|
||||||
|
mutations: Record<string, unknown>;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
provided: Record<string, unknown>;
|
||||||
|
subscriptions: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
||||||
|
'queries',
|
||||||
|
'mutations',
|
||||||
|
'config',
|
||||||
|
'provided',
|
||||||
|
'subscriptions',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard used to select apis from the user store state.
|
||||||
|
* @param val
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isApiSlice(val: unknown): val is RtkQueryApiState {
|
||||||
|
if (!isPlainObject(val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, len = rtkqueryApiStateKeys.length; i < len; i++) {
|
||||||
|
if (!isPlainObject(val[rtkqueryApiStateKeys[i]])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiReducerPaths(currentUserState: unknown): string[] | null {
|
||||||
|
if (!isPlainObject(currentUserState)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStateKeys = Object.keys(currentUserState);
|
||||||
|
const output: string[] = [];
|
||||||
|
|
||||||
|
for (const key of userStateKeys) {
|
||||||
|
if (isApiSlice(currentUserState[key])) {
|
||||||
|
output.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownRtkQueryActionPrefixes = oneOfGroup([
|
||||||
|
'executeQuery',
|
||||||
|
'executeMutation',
|
||||||
|
'config',
|
||||||
|
'subscriptions',
|
||||||
|
'invalidation',
|
||||||
|
'mutations',
|
||||||
|
'queries',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a regex that matches rtk query actions from an array of api
|
||||||
|
* `reducerPaths`.
|
||||||
|
* @param reducerPaths list of rtkQuery reducerPaths in user state.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function generateRtkQueryActionRegex(
|
||||||
|
reducerPaths: string[] | null
|
||||||
|
): RegExp | null {
|
||||||
|
if (!reducerPaths?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(
|
||||||
|
`^${oneOfGroup(reducerPaths)}/${knownRtkQueryActionPrefixes}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectRTKQueryActionRegex<S, A extends Action<unknown>> {
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): RegExp | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSelectRtkQueryActionRegex<
|
||||||
|
S,
|
||||||
|
A extends Action<unknown>
|
||||||
|
>(): SelectRTKQueryActionRegex<S, A> {
|
||||||
|
const selectApiReducerPaths = createSelector(
|
||||||
|
(source: SelectorsSource<S, A>) =>
|
||||||
|
source.computedStates[source.currentStateIndex]?.state,
|
||||||
|
getApiReducerPaths
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectRtkQueryActionRegex = createShallowEqualSelector(
|
||||||
|
selectApiReducerPaths,
|
||||||
|
generateRtkQueryActionRegex
|
||||||
|
);
|
||||||
|
|
||||||
|
return selectRtkQueryActionRegex;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
||||||
|
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||||
|
import { DevtoolsInspectorState } from '../redux';
|
||||||
|
import { DevtoolsInspectorProps } from '../DevtoolsInspector';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/reduxjs/reselect#customize-equalitycheck-for-defaultmemoize
|
||||||
|
*/
|
||||||
|
export const createShallowEqualSelector = createSelectorCreator(
|
||||||
|
defaultMemoize,
|
||||||
|
shallowEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
type ComputedStates<S> = LiftedState<
|
||||||
|
S,
|
||||||
|
Action<unknown>,
|
||||||
|
unknown
|
||||||
|
>['computedStates'];
|
||||||
|
|
||||||
|
export interface SelectorsSource<S, A extends Action<unknown>> {
|
||||||
|
readonly actionIds: number[];
|
||||||
|
readonly actions: Record<number, PerformAction<A>>;
|
||||||
|
readonly computedStates: ComputedStates<S>;
|
||||||
|
readonly monitorState: DevtoolsInspectorState;
|
||||||
|
readonly currentStateIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeSelectorSource<S, A extends Action<unknown>>(
|
||||||
|
props: DevtoolsInspectorProps<S, A>,
|
||||||
|
previous: SelectorsSource<S, A> | null = null
|
||||||
|
): SelectorsSource<S, A> {
|
||||||
|
const {
|
||||||
|
computedStates,
|
||||||
|
currentStateIndex,
|
||||||
|
monitorState,
|
||||||
|
stagedActionIds,
|
||||||
|
actionsById,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const next: SelectorsSource<S, A> = {
|
||||||
|
currentStateIndex,
|
||||||
|
monitorState,
|
||||||
|
computedStates,
|
||||||
|
actions: actionsById,
|
||||||
|
actionIds: stagedActionIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (previous && shallowEqual(next, previous)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
function is(x: unknown, y: unknown): boolean {
|
||||||
|
if (x === y) {
|
||||||
|
return x !== 0 || y !== 0 || 1 / x === 1 / y;
|
||||||
|
} else {
|
||||||
|
return x !== x && y !== y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow equal algorithm borrowed from react-redux.
|
||||||
|
* @see https://github.com/reduxjs/react-redux/blob/2c7ef25a0704efcf10e41112d88ae9867e946d10/src/utils/shallowEqual.js
|
||||||
|
*/
|
||||||
|
export default function shallowEqual(objA: unknown, objB: unknown): boolean {
|
||||||
|
if (is(objA, objB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof objA !== 'object' ||
|
||||||
|
objA === null ||
|
||||||
|
typeof objB !== 'object' ||
|
||||||
|
objB === null
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(objA);
|
||||||
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < keysA.length; i++) {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
|
||||||
|
!is(
|
||||||
|
(objA as Record<string, unknown>)[keysA[i]],
|
||||||
|
(objB as Record<string, unknown>)[keysA[i]]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
36
yarn.lock
36
yarn.lock
|
@ -3992,6 +3992,7 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@redux-devtools/core": ^3.9.0
|
"@redux-devtools/core": ^3.9.0
|
||||||
"@redux-devtools/dock-monitor": ^1.4.0
|
"@redux-devtools/dock-monitor": ^1.4.0
|
||||||
|
"@reduxjs/toolkit": ^1.6.0
|
||||||
"@types/dateformat": ^3.0.1
|
"@types/dateformat": ^3.0.1
|
||||||
"@types/dragula": ^3.7.1
|
"@types/dragula": ^3.7.1
|
||||||
"@types/hex-rgba": ^1.0.1
|
"@types/hex-rgba": ^1.0.1
|
||||||
|
@ -4026,6 +4027,7 @@ __metadata:
|
||||||
redux: ^4.1.1
|
redux: ^4.1.1
|
||||||
redux-devtools-themes: ^1.0.0
|
redux-devtools-themes: ^1.0.0
|
||||||
redux-logger: ^3.0.6
|
redux-logger: ^3.0.6
|
||||||
|
reselect: ^4.0.0
|
||||||
seamless-immutable: ^7.1.4
|
seamless-immutable: ^7.1.4
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@redux-devtools/core": ^3.7.0
|
"@redux-devtools/core": ^3.7.0
|
||||||
|
@ -4124,6 +4126,26 @@ __metadata:
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@reduxjs/toolkit@npm:^1.6.0":
|
||||||
|
version: 1.6.1
|
||||||
|
resolution: "@reduxjs/toolkit@npm:1.6.1"
|
||||||
|
dependencies:
|
||||||
|
immer: ^9.0.1
|
||||||
|
redux: ^4.1.0
|
||||||
|
redux-thunk: ^2.3.0
|
||||||
|
reselect: ^4.0.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.14.0 || ^17.0.0
|
||||||
|
react-redux: ^7.2.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
checksum: fc7f8211a74e4ccb246870e9f3dddbd2f9a79ce50ed3c3bb68a59af2b279712e0cba0690479416ef3fea6cfcc1e8d257da8e6a4a49a306d38d83d80182329cfb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@restart/context@npm:^2.1.4":
|
"@restart/context@npm:^2.1.4":
|
||||||
version: 2.1.4
|
version: 2.1.4
|
||||||
resolution: "@restart/context@npm:2.1.4"
|
resolution: "@restart/context@npm:2.1.4"
|
||||||
|
@ -15552,6 +15574,13 @@ fsevents@^1.2.7:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"immer@npm:^9.0.1":
|
||||||
|
version: 9.0.5
|
||||||
|
resolution: "immer@npm:9.0.5"
|
||||||
|
checksum: a7fa984fa1887a33ce6d44a7a505fd5ac76009336d8b1c99d34f59aaefc28aadf93ab1e5db27513acd15be454a8a89d8151e915d9b0b6e86e72acbd28218410b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"immutable@npm:^3.8.1 || ^4.0.0-rc.1, immutable@npm:^4.0.0-rc.12":
|
"immutable@npm:^3.8.1 || ^4.0.0-rc.1, immutable@npm:^4.0.0-rc.12":
|
||||||
version: 4.0.0-rc.12
|
version: 4.0.0-rc.12
|
||||||
resolution: "immutable@npm:4.0.0-rc.12"
|
resolution: "immutable@npm:4.0.0-rc.12"
|
||||||
|
@ -24067,6 +24096,13 @@ fsevents@^1.2.7:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"reselect@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "reselect@npm:4.0.0"
|
||||||
|
checksum: 3480930929f673f12962cdde140dce48ea8ba171cd428bb2c7639672e41770bd6b64e935bc0400f47cfa960f617c7ac068c4309527373825d11e27262f08c0a3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"resolve-cwd@npm:^2.0.0":
|
"resolve-cwd@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "resolve-cwd@npm:2.0.0"
|
resolution: "resolve-cwd@npm:2.0.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user