-
onSearch(e.target.value)}
- placeholder="filter..."
- />
- {!hideMainButtons && (
-
-
-
- {getActiveButtons(hasSkippedActions).map((btn) => (
-
- ({
- Commit: onCommit,
- Sweep: onSweep,
- }[btn]())
- }
- {...styling(
- ['selectorButton', 'selectorButtonSmall'],
- false,
- true
- )}
- >
- {btn}
-
- ))}
-
-
+ actionForm,
+ onActionFormChange,
+}) => {
+ const { isNoopFilterActive, isRtkQueryFilterActive, isInvertSearchActive } =
+ actionForm;
+
+ return (
+ <>
+
+
onSearch(e.target.value)}
+ placeholder="filter..."
+ />
+
+
+
+
+
- )}
-
-);
+ {!hideMainButtons && (
+
+
+
+
+ {getActiveButtons(hasSkippedActions).map((btn) => (
+
+ ({
+ Commit: onCommit,
+ Sweep: onSweep,
+ }[btn]())
+ }
+ {...styling(
+ ['selectorButton', 'selectorButtonSmall'],
+ false,
+ true
+ )}
+ >
+ {btn}
+
+ ))}
+
+
+
+
+ )}
+ >
+ );
+};
ActionListHeader.propTypes = {
styling: PropTypes.func.isRequired,
@@ -70,6 +140,13 @@ ActionListHeader.propTypes = {
hideMainButtons: PropTypes.bool,
hasSkippedActions: 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;
diff --git a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx
index aa9803fa..a2f815f2 100644
--- a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx
+++ b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx
@@ -21,12 +21,17 @@ import ActionList from './ActionList';
import ActionPreview, { Tab } from './ActionPreview';
import getInspectedState from './utils/getInspectedState';
import createDiffPatcher from './createDiffPatcher';
+import debounce from 'lodash.debounce';
import {
+ ActionForm,
+ changeActionFormValues,
DevtoolsInspectorAction,
DevtoolsInspectorState,
reducer,
updateMonitorState,
} from './redux';
+import { makeSelectFilteredActions } from './utils/filters';
+import { computeSelectorSource } from './utils/selectors';
// eslint-disable-next-line @typescript-eslint/unbound-method
const {
@@ -233,6 +238,8 @@ class DevtoolsInspector
> extends PureComponent<
updateSizeTimeout?: number;
inspectorRef?: HTMLDivElement | null;
+ selectorsSource = computeSelectorSource(this.props);
+
componentDidMount() {
this.updateSizeMode();
this.updateSizeTimeout = window.setInterval(
@@ -249,6 +256,10 @@ class DevtoolsInspector> extends PureComponent<
this.props.dispatch(updateMonitorState(monitorState));
};
+ handleActionFormChange = (formValues: Partial) => {
+ this.props.dispatch(changeActionFormValues(formValues));
+ };
+
updateSizeMode() {
const isWideLayout = this.inspectorRef!.offsetWidth > 500;
@@ -286,6 +297,8 @@ class DevtoolsInspector> extends PureComponent<
this.inspectorRef = node;
};
+ selectFilteredActions = makeSelectFilteredActions();
+
render() {
const {
stagedActionIds: actionIds,
@@ -301,7 +314,7 @@ class DevtoolsInspector> extends PureComponent<
hideMainButtons,
hideActionButtons,
} = this.props;
- const { selectedActionId, startActionId, searchValue, tabName } =
+ const { selectedActionId, startActionId, actionForm, tabName } =
monitorState;
const inspectedPathType =
tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath';
@@ -309,6 +322,12 @@ class DevtoolsInspector> extends PureComponent<
this.state;
const { base16Theme, styling } = themeState;
+ this.selectorsSource = computeSelectorSource(
+ this.props,
+ this.selectorsSource
+ );
+ const filteredActionIds = this.selectFilteredActions(this.selectorsSource);
+
return (
> extends PureComponent<
actions,
actionIds,
isWideLayout,
- searchValue,
selectedActionId,
startActionId,
skippedActionIds,
@@ -332,7 +350,10 @@ class DevtoolsInspector
> extends PureComponent<
hideActionButtons,
styling,
}}
+ actionForm={actionForm}
+ filteredActionIds={filteredActionIds}
onSearch={this.handleSearch}
+ onActionFormChange={this.handleActionFormChange}
onSelect={this.handleSelectAction}
onToggleAction={this.handleToggleAction}
onJumpToState={this.handleJumpToState}
@@ -399,9 +420,9 @@ class DevtoolsInspector> extends PureComponent<
this.props.dispatch(sweep());
};
- handleSearch = (val: string) => {
- this.updateMonitorState({ searchValue: val });
- };
+ handleSearch = debounce((val: string) => {
+ this.handleActionFormChange({ searchValue: val });
+ }, 200);
handleSelectAction = (
e: React.MouseEvent,
diff --git a/packages/redux-devtools-inspector-monitor/src/redux.ts b/packages/redux-devtools-inspector-monitor/src/redux.ts
index 93b2835d..2553042d 100644
--- a/packages/redux-devtools-inspector-monitor/src/redux.ts
+++ b/packages/redux-devtools-inspector-monitor/src/redux.ts
@@ -4,17 +4,40 @@ import { DevtoolsInspectorProps } from './DevtoolsInspector';
const 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 {
type: typeof UPDATE_MONITOR_STATE;
monitorState: Partial;
}
+
+export interface ChangeActionFormAction {
+ type: typeof ACTION_FORM_VALUE_CHANGE;
+ formValues: Partial;
+}
+
export function updateMonitorState(
monitorState: Partial
): UpdateMonitorStateAction {
return { type: UPDATE_MONITOR_STATE, monitorState };
}
-export type DevtoolsInspectorAction = UpdateMonitorStateAction;
+export function changeActionFormValues(
+ formValues: Partial
+): ChangeActionFormAction {
+ return { type: ACTION_FORM_VALUE_CHANGE, formValues };
+}
+
+export type DevtoolsInspectorAction =
+ | UpdateMonitorStateAction
+ | ChangeActionFormAction;
export interface DevtoolsInspectorState {
selectedActionId: number | null;
@@ -22,7 +45,7 @@ export interface DevtoolsInspectorState {
inspectedActionPath: (string | number)[];
inspectedStatePath: (string | number)[];
tabName: string;
- searchValue?: string;
+ actionForm: ActionForm;
}
export const DEFAULT_STATE: DevtoolsInspectorState = {
@@ -31,18 +54,35 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
inspectedActionPath: [],
inspectedStatePath: [],
tabName: 'Diff',
+ actionForm: {
+ searchValue: '',
+ isNoopFilterActive: false,
+ isRtkQueryFilterActive: false,
+ isInvertSearchActive: false,
+ },
};
-function reduceUpdateState(
+function internalMonitorActionReducer(
state: DevtoolsInspectorState,
action: DevtoolsInspectorAction
-) {
- return action.type === UPDATE_MONITOR_STATE
- ? {
+): DevtoolsInspectorState {
+ switch (action.type) {
+ case UPDATE_MONITOR_STATE:
+ return {
...state,
...action.monitorState,
- }
- : state;
+ };
+ case ACTION_FORM_VALUE_CHANGE:
+ return {
+ ...state,
+ actionForm: {
+ ...state.actionForm,
+ ...action.formValues,
+ },
+ };
+ default:
+ return state;
+ }
}
export function reducer>(
@@ -51,6 +91,6 @@ export function reducer>(
action: DevtoolsInspectorAction
) {
return {
- ...reduceUpdateState(state, action),
+ ...internalMonitorActionReducer(state, action),
};
}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
index a8e5f88f..f38f59c8 100644
--- a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
+++ b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
@@ -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;
@@ -83,6 +85,11 @@ const getSheetFromColorMap = (map: ColorMap) => ({
'border-color': map.LIST_BORDER_COLOR,
},
+ actionListHeaderSecondRow: {
+ padding: '5px 10px',
+ justifyContent: 'flex-end',
+ },
+
actionListRows: {
overflow: 'auto',
@@ -106,7 +113,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({
actionListHeaderSelector: {
display: 'inline-flex',
- 'margin-right': '10px',
},
actionListWide: {
@@ -330,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',
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts
new file mode 100644
index 00000000..ec3f8008
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts
@@ -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 = LiftedState<
+ S,
+ Action,
+ unknown
+>['computedStates'];
+
+function isNoopAction(
+ actionId: number,
+ computedStates: ComputedStates
+): boolean {
+ return (
+ actionId === 0 ||
+ computedStates[actionId]?.state === computedStates[actionId - 1]?.state
+ );
+}
+
+function filterStateChangingAction(
+ actionIds: number[],
+ computedStates: ComputedStates
+): number[] {
+ return actionIds.filter(
+ (actionId) => !isNoopAction(actionId, computedStates)
+ );
+}
+
+function filterActionsBySearchValue>(
+ searchValue: string | undefined,
+ actionIds: number[],
+ actions: Record>
+): 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>>,
+ 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> {
+ readonly actionIds: number[];
+ readonly actions: Record>;
+ readonly computedStates: ComputedStates;
+ readonly actionForm: ActionForm;
+ readonly rtkQueryRegex: RegExp | null;
+}
+
+function filterActions>({
+ actionIds,
+ actions,
+ computedStates,
+ actionForm,
+ rtkQueryRegex,
+}: FilterActionsPayload): 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> {
+ (selectorsSource: SelectorsSource): number[];
+}
+
+/**
+ * Creates a selector that given `SelectorsSource` returns
+ * a list of filtered `actionsIds`.
+ * @returns {number[]}
+ */
+export function makeSelectFilteredActions<
+ S,
+ A extends Action
+>(): SelectFilteredActions {
+ const selectRegex = makeSelectRtkQueryActionRegex();
+
+ return createShallowEqualSelector(
+ (selectorsSource: SelectorsSource): FilterActionsPayload => {
+ const actionForm = selectorsSource.monitorState.actionForm;
+ const { actionIds, actions, computedStates } = selectorsSource;
+
+ return {
+ actionIds,
+ actions,
+ computedStates,
+ actionForm,
+ rtkQueryRegex: selectRegex(selectorsSource),
+ };
+ },
+ filterActions
+ );
+}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/object.ts b/packages/redux-devtools-inspector-monitor/src/utils/object.ts
new file mode 100644
index 00000000..98b886f6
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/object.ts
@@ -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 {
+ 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(val: T): T {
+ return val;
+}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts b/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts
new file mode 100644
index 00000000..5901ecb2
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts
@@ -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('|')})`;
+}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
new file mode 100644
index 00000000..de4cefcd
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
@@ -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;
+ mutations: Record;
+ config: Record;
+ provided: Record;
+ subscriptions: Record;
+}
+
+const rtkqueryApiStateKeys: ReadonlyArray = [
+ '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> {
+ (selectorsSource: SelectorsSource): RegExp | null;
+}
+
+export function makeSelectRtkQueryActionRegex<
+ S,
+ A extends Action
+>(): SelectRTKQueryActionRegex {
+ const selectApiReducerPaths = createSelector(
+ (source: SelectorsSource) =>
+ source.computedStates[source.currentStateIndex]?.state,
+ getApiReducerPaths
+ );
+
+ const selectRtkQueryActionRegex = createShallowEqualSelector(
+ selectApiReducerPaths,
+ generateRtkQueryActionRegex
+ );
+
+ return selectRtkQueryActionRegex;
+}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts b/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts
new file mode 100644
index 00000000..790e83dc
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts
@@ -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 = LiftedState<
+ S,
+ Action,
+ unknown
+>['computedStates'];
+
+export interface SelectorsSource> {
+ readonly actionIds: number[];
+ readonly actions: Record>;
+ readonly computedStates: ComputedStates;
+ readonly monitorState: DevtoolsInspectorState;
+ readonly currentStateIndex: number;
+}
+
+export function computeSelectorSource>(
+ props: DevtoolsInspectorProps,
+ previous: SelectorsSource | null = null
+): SelectorsSource {
+ const {
+ computedStates,
+ currentStateIndex,
+ monitorState,
+ stagedActionIds,
+ actionsById,
+ } = props;
+
+ const next: SelectorsSource = {
+ currentStateIndex,
+ monitorState,
+ computedStates,
+ actions: actionsById,
+ actionIds: stagedActionIds,
+ };
+
+ if (previous && shallowEqual(next, previous)) {
+ return previous;
+ }
+
+ return next;
+}
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts b/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts
new file mode 100644
index 00000000..536ffd1d
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts
@@ -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)[keysA[i]],
+ (objB as Record)[keysA[i]]
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/yarn.lock b/yarn.lock
index d8b3b22b..9267007f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3992,6 +3992,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.1
"@types/hex-rgba": ^1.0.1
@@ -4026,6 +4027,7 @@ __metadata:
redux: ^4.1.1
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
@@ -4124,6 +4126,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"
@@ -15552,6 +15574,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"
@@ -24067,6 +24096,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"