feat(inspector-monitor): add rtk-query toggle button #753

This commit is contained in:
FaberVitale 2021-07-24 17:38:09 +02:00
parent 229869698b
commit 0af1d2b301
21 changed files with 763 additions and 48 deletions

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -1,6 +1,6 @@
import React, { CSSProperties } from 'react';
import { connect } from 'react-redux';
import pkg from '../../../package.json';
import pkg from '../../../../package.json';
import Button from 'react-bootstrap/Button';
import FormGroup from 'react-bootstrap/FormGroup';
import FormControl from 'react-bootstrap/FormControl';
@ -11,9 +11,10 @@ import InputGroup from 'react-bootstrap/InputGroup';
import Row from 'react-bootstrap/Row';
import * as base16 from 'base16';
import { push as pushRoute } from 'connected-react-router';
import { PokemonView } from '../components/PokemonView';
import { Path } from 'history';
import * as inspectorThemes from '../../../src/themes';
import getOptions, { Options } from './getOptions';
import * as inspectorThemes from '../../../../src/themes';
import getOptions, { Options } from '../getOptions';
import {
AddFunctionAction,
AddHugeObjectAction,
@ -34,7 +35,7 @@ import {
ShuffleArrayAction,
TimeoutUpdateAction,
ToggleTimeoutUpdateAction,
} from './reducers';
} from '../reducers';
const styles: {
wrapper: CSSProperties;
@ -56,9 +57,10 @@ const styles: {
header: {},
content: {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '50%',
justifyContent: 'space-evenly',
padding: 8,
},
buttons: {
display: 'flex',
@ -251,6 +253,7 @@ class DemoApp extends React.Component<Props> {
Shuffle Array
</Button>
</div>
<PokemonView />
</div>
<div style={styles.links}>
<a onClick={this.toggleExtension} style={styles.link}>

View File

@ -3,10 +3,10 @@ import { connect } from 'react-redux';
import { createDevTools } from '@redux-devtools/core';
import DockMonitor from '@redux-devtools/dock-monitor';
import { Location } from 'history';
import DevtoolsInspector from '../../../src/DevtoolsInspector';
import getOptions from './getOptions';
import { base16Themes } from '../../../src/utils/createStylingFromTheme';
import { DemoAppState } from './reducers';
import DevtoolsInspector from '../../../../src/DevtoolsInspector';
import getOptions from '../getOptions';
import { base16Themes } from '../../../../src/utils/createStylingFromTheme';
import { DemoAppState } from '../reducers';
const CustomComponent = () => (
<div

View File

@ -1,8 +1,9 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { pokemonApi } from '../js/rtk-query/pokemonApi';
import {
createStore,
applyMiddleware,
compose,
StoreEnhancerStoreCreator,
@ -13,10 +14,10 @@ import { Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
import { persistState } from '@redux-devtools/core';
import DemoApp from './DemoApp';
import DemoApp from './containers/DemoApp';
import createRootReducer from './reducers';
import getOptions from './getOptions';
import { ConnectedDevTools, getDevTools } from './DevTools';
import { ConnectedDevTools, getDevTools } from './containers/DevTools';
function getDebugSessionKey() {
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
@ -51,7 +52,16 @@ const enhancer = compose(
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(
<Provider store={store}>

View File

@ -7,6 +7,7 @@ import {
RouterState,
} from 'connected-react-router';
import { History } from 'history';
import { pokemonApi } from './rtk-query/pokemonApi';
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;
addSymbol: { s: symbol; error: Error } | null;
shuffleArray: unknown[];
[pokemonApi.reducerPath]: ReturnType<typeof pokemonApi.reducer>;
}
const createRootReducer = (
@ -259,6 +261,7 @@ const createRootReducer = (
: state,
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
[pokemonApi.reducerPath]: pokemonApi.reducer,
});
export default createRootReducer;

View File

@ -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];

View File

@ -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;

View File

@ -51,11 +51,13 @@
"react-base16-styling": "^0.8.0",
"react-dragula": "^1.1.17",
"react-json-tree": "^0.15.0",
"redux-devtools-themes": "^1.0.0"
"redux-devtools-themes": "^1.0.0",
"reselect": "^4.0.0"
},
"devDependencies": {
"@redux-devtools/core": "^3.9.0",
"@redux-devtools/dock-monitor": "^1.4.0",
"@reduxjs/toolkit": "^1.6.0",
"@types/dateformat": "^3.0.1",
"@types/hex-rgba": "^1.0.0",
"@types/history": "^4.7.8",

View File

@ -6,7 +6,6 @@ import { PerformAction } from '@redux-devtools/core';
import { StylingFunction } from 'react-base16-styling';
import ActionListRow from './ActionListRow';
import ActionListHeader from './ActionListHeader';
import { filterActions } from './utils/filters';
import { ActionForm } from './redux';
function getTimestamps<A extends Action<unknown>>(
@ -30,9 +29,9 @@ interface Props<S, A extends Action<unknown>> {
selectedActionId: number | null;
startActionId: number | null;
skippedActionIds: number[];
computedStates: { state: S; error?: string }[];
draggableActions: boolean;
actionForm: ActionForm;
filteredActionIds: number[];
hideMainButtons: boolean | undefined;
hideActionButtons: boolean | undefined;
styling: StylingFunction;
@ -129,12 +128,11 @@ export default class ActionList<
hideActionButtons,
onCommit,
onSweep,
actionForm,
onActionFormChange,
onJumpToState,
actionForm,
filteredActionIds,
} = this.props;
const filteredActionIds = filterActions<unknown, A>(this.props);
return (
<div
key="actionList"
@ -155,7 +153,7 @@ export default class ActionList<
hasStagedActions={actionIds.length > 1}
/>
<div {...styling('actionListRows')} ref={this.getRef}>
{filteredActionIds.map((actionId) => (
{filteredActionIds.map((actionId: number) => (
<ActionListRow
key={actionId}
styling={styling}

View File

@ -32,7 +32,7 @@ const ActionListHeader: FunctionComponent<Props> = ({
actionForm,
onActionFormChange,
}) => {
const { isNoopFilterActive } = actionForm;
const { isNoopFilterActive, isRtkQueryFilterActive } = actionForm;
return (
<>
@ -42,7 +42,37 @@ const ActionListHeader: FunctionComponent<Props> = ({
onChange={(e) => onSearch(e.target.value)}
placeholder="filter..."
/>
{!hideMainButtons && (
<div {...styling('toggleButtonWrapper')}>
<button
title="Toggle visibility of noop actions"
aria-label="Toggle visibility of noop actions"
aria-pressed={!isNoopFilterActive}
onClick={() =>
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
}
type="button"
{...styling('toggleButton')}
>
noop
</button>
<button
title="Toggle visibility of rtk-query actions"
aria-label="Toggle visibility of rtk-query actions"
aria-pressed={!isRtkQueryFilterActive}
onClick={() =>
onActionFormChange({
isRtkQueryFilterActive: !isRtkQueryFilterActive,
})
}
type="button"
{...styling('toggleButton')}
>
RTKQ
</button>
</div>
</div>
{!hideMainButtons && (
<div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
<div {...styling('actionListHeaderWrapper')}>
<RightSlider shown={hasStagedActions} styling={styling}>
<div {...styling('actionListHeaderSelector')}>
@ -67,25 +97,8 @@ const ActionListHeader: FunctionComponent<Props> = ({
</div>
</RightSlider>
</div>
)}
</div>
<div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
<button
title="Toggle visibility of noop actions"
aria-label="Toggle visibility of noop actions"
aria-pressed={!isNoopFilterActive}
onClick={() =>
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
}
type="button"
{...styling(
['selectorButton', 'selectorButtonSmall', !isNoopFilterActive && 'selectorButtonSelected'],
isNoopFilterActive
)}
>
noop
</button>
</div>
</div>
)}
</>
);
};
@ -101,6 +114,7 @@ ActionListHeader.propTypes = {
actionForm: PropTypes.shape({
searchValue: PropTypes.string.isRequired,
isNoopFilterActive: PropTypes.bool.isRequired,
isRtkQueryFilterActive: PropTypes.bool.isRequired,
}).isRequired,
onActionFormChange: PropTypes.func.isRequired,
};

View File

@ -30,6 +30,8 @@ import {
reducer,
updateMonitorState,
} from './redux';
import { makeSelectFilteredActions } from './utils/filters';
import { computeSelectorSource } from './utils/selectors';
// eslint-disable-next-line @typescript-eslint/unbound-method
const {
@ -236,6 +238,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
updateSizeTimeout?: number;
inspectorRef?: HTMLDivElement | null;
selectorsSource = computeSelectorSource(this.props);
componentDidMount() {
this.updateSizeMode();
this.updateSizeTimeout = window.setInterval(
@ -293,6 +297,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
this.inspectorRef = node;
};
selectFilteredActions = makeSelectFilteredActions();
render() {
const {
stagedActionIds: actionIds,
@ -316,6 +322,12 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
this.state;
const { base16Theme, styling } = themeState;
this.selectorsSource = computeSelectorSource(
this.props,
this.selectorsSource
);
const filteredActionIds = this.selectFilteredActions(this.selectorsSource);
return (
<div
key="inspector"
@ -339,7 +351,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
styling,
}}
actionForm={actionForm}
computedStates={computedStates}
filteredActionIds={filteredActionIds}
onSearch={this.handleSearch}
onActionFormChange={this.handleActionFormChange}
onSelect={this.handleSelectAction}

View File

@ -10,6 +10,7 @@ const ACTION_FORM_VALUE_CHANGE =
export interface ActionForm {
searchValue: string;
isNoopFilterActive: boolean;
isRtkQueryFilterActive: boolean;
}
export interface UpdateMonitorStateAction {
type: typeof UPDATE_MONITOR_STATE;
@ -55,6 +56,7 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
actionForm: {
searchValue: '',
isNoopFilterActive: false,
isRtkQueryFilterActive: false,
},
};

View File

@ -1,4 +1,4 @@
import jss, { Styles, StyleSheet } from 'jss';
import jss, { StyleSheet } from 'jss';
import preset from 'jss-preset-default';
import { createStyling } from 'react-base16-styling';
import rgba from 'hex-rgba';
@ -33,6 +33,8 @@ const colorMap = (theme: Base16Theme) => ({
LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08,
TOGGLE_BUTTON_BACKGROUND: rgba(theme.base00, 70),
TOGGLE_BUTTON_SELECTED_BACKGROUND: theme.base04,
});
type Color = keyof ReturnType<typeof colorMap>;
@ -85,6 +87,7 @@ const getSheetFromColorMap = (map: ColorMap) => ({
actionListHeaderSecondRow: {
padding: '5px 10px',
justifyContent: 'flex-end',
},
actionListRows: {
@ -110,7 +113,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({
actionListHeaderSelector: {
display: 'inline-flex',
'margin-right': '10px',
},
actionListWide: {
@ -334,6 +336,55 @@ const getSheetFromColorMap = (map: ColorMap) => ({
'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: {
padding: '2px 3px',
'border-radius': '3px',

View File

@ -1,6 +1,8 @@
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,
@ -34,7 +36,7 @@ function filterActionsBySearchValue<A extends Action<unknown>>(
): number[] {
const lowerSearchValue = searchValue && searchValue.toLowerCase();
if (!lowerSearchValue) {
if (!lowerSearchValue || !actionIds.length) {
return actionIds;
}
@ -48,18 +50,35 @@ function filterActionsBySearchValue<A extends Action<unknown>>(
});
}
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);
});
}
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;
}
export function filterActions<S, A extends Action<unknown>>({
function filterActions<S, A extends Action<unknown>>({
actionIds,
actions,
computedStates,
actionForm,
rtkQueryRegex,
}: FilterActionsPayload<S, A>): number[] {
let output = filterActionsBySearchValue(
actionForm.searchValue,
@ -68,8 +87,44 @@ export function filterActions<S, A extends Action<unknown>>({
);
if (actionForm.isNoopFilterActive) {
output = filterStateChangingAction(actionIds, computedStates);
output = filterStateChangingAction(output, computedStates);
}
if (actionForm.isRtkQueryFilterActive && rtkQueryRegex) {
output = filterOutRtkQueryActions(output, actions, rtkQueryRegex);
}
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
);
}

View File

@ -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;
}

View File

@ -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('|')})`;
}

View 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -3598,6 +3598,7 @@ __metadata:
dependencies:
"@redux-devtools/core": ^3.9.0
"@redux-devtools/dock-monitor": ^1.4.0
"@reduxjs/toolkit": ^1.6.0
"@types/dateformat": ^3.0.1
"@types/dragula": ^3.7.0
"@types/hex-rgba": ^1.0.0
@ -3632,6 +3633,7 @@ __metadata:
redux: ^4.1.0
redux-devtools-themes: ^1.0.0
redux-logger: ^3.0.6
reselect: ^4.0.0
seamless-immutable: ^7.1.4
peerDependencies:
"@redux-devtools/core": ^3.7.0
@ -3730,6 +3732,26 @@ __metadata:
languageName: unknown
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":
version: 2.1.4
resolution: "@restart/context@npm:2.1.4"
@ -14909,6 +14931,13 @@ fsevents@^1.2.7:
languageName: node
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":
version: 4.0.0-rc.12
resolution: "immutable@npm:4.0.0-rc.12"
@ -23416,6 +23445,13 @@ fsevents@^1.2.7:
languageName: node
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":
version: 2.0.0
resolution: "resolve-cwd@npm:2.0.0"