refactor(rtk-query): move Inspector & Monitor to containers clean up typings

Other changes:

* chore: improved type coverage

* chore: do not lint packages/redux-devtools-rtk-query-monitor/demo folder

* refactor: put sort order buttons inside a component

* chore: hopefully resolve mockServiceWorker formatting issues
This commit is contained in:
FaberVitale 2021-06-21 15:34:18 +02:00
parent 9f1d718e80
commit e84c0dcd99
19 changed files with 261 additions and 230 deletions

View File

@ -10,4 +10,4 @@ __snapshots__
dev
.yarn/*
**/.yarn/*
demo/public/**
**/demo/public/**

View File

@ -1,5 +1,2 @@
lib
demo/public/**
demo/src/mocks/**
demo/src/build/**
demo/build/**
demo/

View File

@ -1,9 +1,9 @@
module.exports = {
extends: '../../../.eslintrc',
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../../eslintrc.ts.react.base.json',
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],

View File

@ -7,116 +7,116 @@
/* eslint-disable */
/* tslint:disable */
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187';
const bypassHeaderName = 'x-msw-bypass';
const activeClientIds = new Set();
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
const bypassHeaderName = 'x-msw-bypass'
const activeClientIds = new Set()
self.addEventListener('install', function () {
return self.skipWaiting();
});
return self.skipWaiting()
})
self.addEventListener('activate', async function (event) {
return self.clients.claim();
});
return self.clients.claim()
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
const clientId = event.source.id
if (!clientId || !self.clients) {
return;
return
}
const client = await self.clients.get(clientId);
const client = await self.clients.get(clientId)
if (!client) {
return;
return
}
const allClients = await self.clients.matchAll();
const allClients = await self.clients.matchAll()
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
});
break;
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
});
break;
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId);
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
});
break;
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
self.registration.unregister()
}
break;
break
}
}
});
})
// Resolve the "master" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMasterClient(event) {
const client = await self.clients.get(event.clientId);
const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') {
return client;
return client
}
const allClients = await self.clients.matchAll();
const allClients = await self.clients.matchAll()
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible';
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
return activeClientIds.has(client.id)
})
}
async function handleRequest(event, requestId) {
const client = await resolveMasterClient(event);
const response = await getResponse(event, client, requestId);
const client = await resolveMasterClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const clonedResponse = response.clone();
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
@ -130,21 +130,21 @@ async function handleRequest(event, requestId) {
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
})
})()
}
return response;
return response
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
const { request } = event
const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone)
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
return getOriginalResponse()
}
// Bypass initial page load requests (i.e. static assets).
@ -152,26 +152,26 @@ async function getResponse(event, client, requestId) {
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse();
return await getOriginalResponse()
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
delete cleanRequestHeaders[bypassHeaderName]
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
})
return fetch(originalRequest);
return fetch(originalRequest)
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const reqHeaders = serializeHeaders(request.headers)
const body = await request.text()
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
@ -192,31 +192,31 @@ async function getResponse(event, client, requestId) {
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
})
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(
() => respondWithMock(clientMessage),
clientMessage.payload.delay
);
clientMessage.payload.delay,
)
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse();
return getOriginalResponse()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name
// Rejecting a request Promise emulates a network error.
throw networkError;
throw networkError
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body);
const parsedBody = JSON.parse(clientMessage.payload.body)
console.error(
`\
@ -229,38 +229,38 @@ This exception has been gracefully handled as a 500 response, however, it's stro
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
request.url,
)
return respondWithMock(clientMessage);
return respondWithMock(clientMessage)
}
}
return getOriginalResponse();
return getOriginalResponse()
}
self.addEventListener('fetch', function (event) {
const { request } = event;
const { request } = event
// Bypass navigation requests.
if (request.mode === 'navigate') {
return;
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
return
}
const requestId = uuidv4();
const requestId = uuidv4()
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
@ -268,55 +268,55 @@ self.addEventListener('fetch', function (event) {
'[MSW] Failed to mock a "%s" request to "%s": %s',
request.method,
request.url,
error
);
})
);
});
error,
)
}),
)
})
function serializeHeaders(headers) {
const reqHeaders = {};
const reqHeaders = {}
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value;
});
return reqHeaders;
: value
})
return reqHeaders
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
return reject(event.data.error)
}
resolve(event.data);
};
resolve(event.data)
}
client.postMessage(JSON.stringify(message), [channel.port2]);
});
client.postMessage(JSON.stringify(message), [channel.port2])
})
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
setTimeout(() => resolve(cb()), duration)
})
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
})
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
const r = (Math.random() * 16) | 0
const v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@ -73,7 +73,7 @@ const PostJsonDetail = ({ id }: { id: string }) => {
export const PostDetail = () => {
const { id } = useParams<{ id: any }>();
const { push } = useHistory();
const history = useHistory();
const toast = useToast();
@ -137,7 +137,9 @@ export const PostDetail = () => {
{isUpdating ? 'Updating...' : 'Edit'}
</Button>
<Button
onClick={() => deletePost(id).then(() => push('/posts'))}
onClick={() =>
deletePost(id).then(() => history.push('/posts'))
}
disabled={isDeleting}
colorScheme="red"
>

View File

@ -82,7 +82,7 @@ const AddPost = () => {
const PostList = () => {
const { data: posts, isLoading } = useGetPostsQuery();
const { push } = useHistory();
const history = useHistory();
if (isLoading) {
return <div>Loading</div>;
@ -95,7 +95,7 @@ const PostList = () => {
return (
<List spacing={3}>
{posts.map(({ id, name }) => (
<ListItem key={id} onClick={() => push(`/posts/${id}`)}>
<ListItem key={id} onClick={() => history.push(`/posts/${id}`)}>
<ListIcon as={MdBook} color="green.500" /> {name}
</ListItem>
))}

View File

@ -1,13 +1,14 @@
import React from 'react';
import { ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
export function NoRtkQueryApi(): ReactNode {
export function NoRtkQueryApi(): JSX.Element {
return (
<StyleUtilsContext.Consumer>
{({ styling }) => (
<div {...styling('noApiFound')}>
No rtk-query api found.<br/>Make sure to follow{' '}
No rtk-query api found.
<br />
Make sure to follow{' '}
<a
href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage"
target="_blank"

View File

@ -6,6 +6,7 @@ import { SelectOption } from '../types';
import debounce from 'lodash.debounce';
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
import { QueryFilters, filterQueryOptions } from '../utils/filters';
import { SortOrderButtons } from './SortOrderButtons';
export interface QueryFormProps {
values: QueryFormValues;
@ -16,8 +17,6 @@ interface QueryFormState {
searchValue: string;
}
const ascId = 'rtk-query-rb-asc';
const descId = 'rtk-query-rb-desc';
const selectId = 'rtk-query-comp-select';
const searchId = 'rtk-query-search-query';
const filterSelectId = 'rtk-query-search-query-select';
@ -41,19 +40,8 @@ export class QueryForm extends React.PureComponent<
evt.preventDefault();
};
handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => {
const {
values: { isAscendingQueryComparatorOrder: isAsc },
onFormValuesChange,
} = this.props;
const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) {
onFormValuesChange({ isAscendingQueryComparatorOrder: true });
} else if (targetId === descId && isAsc) {
onFormValuesChange({ isAscendingQueryComparatorOrder: false });
}
handleButtonGroupClick = (isAsc: boolean): void => {
this.props.onFormValuesChange({ isAscendingQueryComparatorOrder: isAsc });
};
handleSelectComparatorChange = (
@ -111,8 +99,6 @@ export class QueryForm extends React.PureComponent<
},
} = this.props;
const isDesc = !isAsc;
return (
<StyleUtilsContext.Consumer>
{({ styling, base16Theme }) => {
@ -170,37 +156,10 @@ export class QueryForm extends React.PureComponent<
options={sortQueryOptions}
onChange={this.handleSelectComparatorChange}
/>
<div
tabIndex={0}
role="radiogroup"
aria-activedescendant={isAsc ? ascId : descId}
onClick={this.handleButtonGroupClick}
>
<button
role="radio"
type="button"
id={ascId}
aria-checked={isAsc}
{...styling(
['selectorButton', isAsc && 'selectorButtonSelected'],
isAsc
)}
>
asc
</button>
<button
id={descId}
role="radio"
type="button"
aria-checked={isDesc}
{...styling(
['selectorButton', isDesc && 'selectorButtonSelected'],
isDesc
)}
>
desc
</button>
</div>
<SortOrderButtons
isAsc={isAsc}
onChange={this.handleButtonGroupClick}
/>
</div>
</form>
);

View File

@ -0,0 +1,67 @@
import React, { MouseEvent } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
export const ascId = 'rtk-query-rb-asc';
export const descId = 'rtk-query-rb-desc';
export interface SortOrderButtonsProps {
readonly isAsc?: boolean;
readonly onChange: (isAsc: boolean) => void;
}
export function SortOrderButtons({
isAsc,
onChange,
}: SortOrderButtonsProps): JSX.Element {
const handleButtonGroupClick = ({
target,
}: MouseEvent<HTMLElement>): void => {
const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) {
onChange(true);
} else if (targetId === descId && isAsc) {
onChange(false);
}
};
const isDesc = !isAsc;
return (
<StyleUtilsContext.Consumer>
{({ styling }) => (
<div
tabIndex={0}
role="radiogroup"
aria-activedescendant={isAsc ? ascId : descId}
onClick={handleButtonGroupClick}
>
<button
role="radio"
type="button"
id={ascId}
aria-checked={isAsc}
{...styling(
['selectorButton', isAsc && 'selectorButtonSelected'],
isAsc
)}
>
asc
</button>
<button
id={descId}
role="radio"
type="button"
aria-checked={isDesc}
{...styling(
['selectorButton', isDesc && 'selectorButtonSelected'],
isDesc
)}
>
desc
</button>
</div>
)}
</StyleUtilsContext.Consumer>
);
}

View File

@ -1,37 +1,33 @@
import React, { Component, createRef, ReactNode } from 'react';
import { AnyAction, Dispatch, Action } from 'redux';
import { LiftedAction, LiftedState } from '@redux-devtools/core';
import * as themes from 'redux-devtools-themes';
import { Base16Theme } from 'react-base16-styling';
import React, { PureComponent, createRef, ReactNode } from 'react';
import type { AnyAction, Dispatch, Action } from '@reduxjs/toolkit';
import type { LiftedAction, LiftedState } from '@redux-devtools/core';
import {
QueryFormValues,
QueryInfo,
QueryPreviewTabs,
RtkQueryMonitorState,
StyleUtils,
} from './types';
import { createInspectorSelectors, computeSelectorSource } from './selectors';
SelectorsSource,
} from '../types';
import { createInspectorSelectors, computeSelectorSource } from '../selectors';
import {
changeQueryFormValues,
selectedPreviewTab,
selectQueryKey,
} from './reducers';
import { QueryList } from './components/QueryList';
import { QueryForm } from './components/QueryForm';
import { QueryPreview } from './components/QueryPreview';
import { getApiStateOf, getQuerySubscriptionsOf } from './utils/rtk-query';
} from '../reducers';
import { QueryList } from '../components/QueryList';
import { QueryForm } from '../components/QueryForm';
import { QueryPreview } from '../components/QueryPreview';
import { getApiStateOf, getQuerySubscriptionsOf } from '../utils/rtk-query';
type SelectorsSource<S> = {
userState: S | null;
monitorState: RtkQueryMonitorState;
};
type ForwardedMonitorProps<S, A extends Action<unknown>> = Pick<
LiftedState<S, A, RtkQueryMonitorState>,
'monitorState' | 'currentStateIndex' | 'computedStates'
>;
export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
extends LiftedState<S, A, RtkQueryMonitorState> {
extends ForwardedMonitorProps<S, A> {
dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>;
theme: keyof typeof themes | Base16Theme;
invertTheme: boolean;
state: S | null;
styleUtils: StyleUtils;
}
@ -40,13 +36,13 @@ type RtkQueryInspectorState<S> = {
isWideLayout: boolean;
};
class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
RtkQueryInspectorProps<S, A>,
RtkQueryInspectorState<S>
> {
inspectorRef = createRef<HTMLDivElement>();
isWideIntervalRef: number | NodeJS.Timeout | null = null;
isWideIntervalRef: ReturnType<typeof setInterval> | null = null;
constructor(props: RtkQueryInspectorProps<S, A>) {
super(props);
@ -98,7 +94,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
componentWillUnmount(): void {
if (this.isWideIntervalRef) {
clearTimeout(this.isWideIntervalRef as any);
clearTimeout(this.isWideIntervalRef);
}
}

View File

@ -1,18 +1,18 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Action } from 'redux';
import { Action, AnyAction } from 'redux';
import RtkQueryInspector from './RtkQueryInspector';
import { reducer } from './reducers';
import { reducer } from '../reducers';
import {
ExternalProps,
RtkQueryMonitorProps,
RtkQueryMonitorState,
StyleUtils,
} from './types';
} from '../types';
import {
createThemeState,
StyleUtilsContext,
} from './styles/createStylingFromTheme';
} from '../styles/createStylingFromTheme';
interface DefaultProps {
theme: string;
@ -41,7 +41,7 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
invertTheme: PropTypes.bool,
};
static defaultProps = {
static defaultProps: DefaultProps = {
theme: 'nicinabox',
invertTheme: false,
};
@ -55,17 +55,16 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
}
render() {
const {
styleUtils: { base16Theme },
} = this.state;
const RtkQueryInspectorAsAny = RtkQueryInspector as any;
const { currentStateIndex, computedStates, monitorState, dispatch } =
this.props;
return (
<StyleUtilsContext.Provider value={this.state.styleUtils}>
<RtkQueryInspectorAsAny
{...this.props}
theme={base16Theme}
<RtkQueryInspector<S, AnyAction>
computedStates={computedStates}
currentStateIndex={currentStateIndex}
monitorState={monitorState}
dispatch={dispatch}
styleUtils={this.state.styleUtils}
/>
</StyleUtilsContext.Provider>

View File

@ -1,2 +1,2 @@
export { default } from './RtkQueryMonitor';
export { default } from './containers/RtkQueryMonitor';
export { ExternalProps } from './types';

View File

@ -1,10 +1,10 @@
import { Action, AnyAction } from 'redux';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector';
import {
QueryInfo,
RtkQueryMonitorState,
QueryFormValues,
RtkQueryMonitorProps,
QueryPreviewTabs,
} from './types';
import { QueryComparators } from './utils/comparators';
@ -53,7 +53,7 @@ const monitorSlice = createSlice({
});
export function reducer<S, A extends Action<unknown>>(
props: RtkQueryInspectorProps<S, A>,
props: RtkQueryMonitorProps<S, A>,
state: RtkQueryMonitorState | undefined,
action: AnyAction
): RtkQueryMonitorState {

View File

@ -1,5 +1,5 @@
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { RtkQueryInspectorProps } from './containers/RtkQueryInspector';
import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types';
import { Comparator, queryComparators } from './utils/comparators';
import { FilterList, queryListFilters } from './utils/filters';

View File

@ -14,31 +14,32 @@ import { createContext } from 'react';
jss.setup(preset());
export const colorMap = (theme: reduxThemes.Base16Theme) => ({
TEXT_COLOR: theme.base06,
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
BACKGROUND_COLOR: theme.base00,
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
BORDER_COLOR: rgba(theme.base03, 50),
LIST_BORDER_COLOR: rgba(theme.base03, 50),
ACTION_TIME_BACK_COLOR: rgba(theme.base03, 20),
ACTION_TIME_COLOR: theme.base04,
PIN_COLOR: theme.base04,
ITEM_HINT_COLOR: rgba(theme.base0F, 90),
TAB_BACK_SELECTED_COLOR: rgba(theme.base03, 20),
TAB_BACK_COLOR: rgba(theme.base00, 70),
TAB_BACK_HOVER_COLOR: rgba(theme.base03, 40),
TAB_BORDER_COLOR: rgba(theme.base03, 50),
DIFF_ADD_COLOR: rgba(theme.base0B, 40),
DIFF_REMOVE_COLOR: rgba(theme.base08, 40),
DIFF_ARROW_COLOR: theme.base0E,
LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08,
});
export const colorMap = (theme: reduxThemes.Base16Theme) =>
({
TEXT_COLOR: theme.base06,
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
BACKGROUND_COLOR: theme.base00,
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
BORDER_COLOR: rgba(theme.base03, 50),
LIST_BORDER_COLOR: rgba(theme.base03, 50),
ACTION_TIME_BACK_COLOR: rgba(theme.base03, 20),
ACTION_TIME_COLOR: theme.base04,
PIN_COLOR: theme.base04,
ITEM_HINT_COLOR: rgba(theme.base0F, 90),
TAB_BACK_SELECTED_COLOR: rgba(theme.base03, 20),
TAB_BACK_COLOR: rgba(theme.base00, 70),
TAB_BACK_HOVER_COLOR: rgba(theme.base03, 40),
TAB_BORDER_COLOR: rgba(theme.base03, 50),
DIFF_ADD_COLOR: rgba(theme.base0B, 40),
DIFF_REMOVE_COLOR: rgba(theme.base08, 40),
DIFF_ARROW_COLOR: theme.base0E,
LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08,
} as const);
type Color = keyof ReturnType<typeof colorMap>;
type ColorMap = {

View File

@ -5,11 +5,11 @@ import isIterable from '../utils/isIterable';
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
function isImmutable(value: any) {
function isImmutable(value: unknown) {
return isKeyed(value) || isIndexed(value) || isCollection(value);
}
function getShortTypeString(val: any, diff: boolean | undefined) {
function getShortTypeString(val: unknown, diff: boolean | undefined) {
if (diff && Array.isArray(val)) {
val = val[val.length === 2 ? 1 : 0];
}
@ -23,7 +23,9 @@ function getShortTypeString(val: any, diff: boolean | undefined) {
} else if (val === undefined) {
return 'undef';
} else if (typeof val === 'object') {
return Object.keys(val).length > 0 ? '{…}' : '{}';
return Object.keys(val as Record<string, unknown>).length > 0
? '{…}'
: '{}';
} else if (typeof val === 'function') {
return 'fn';
} else if (typeof val === 'string') {

View File

@ -1,11 +1,11 @@
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
import type { LiftedAction, LiftedState } from '@redux-devtools/instrument';
import type { createApi, QueryStatus } from '@reduxjs/toolkit/query';
import { ComponentType, Dispatch } from 'react';
import { Base16Theme, StylingFunction } from 'react-base16-styling';
import { Action } from 'redux';
import * as themes from 'redux-devtools-themes';
import { QueryComparators } from './utils/comparators';
import { QueryFilters } from './utils/filters';
import type { Action, Dispatch } from '@reduxjs/toolkit';
import type { ComponentType } from 'react';
import type { Base16Theme, StylingFunction } from 'react-base16-styling';
import type * as themes from 'redux-devtools-themes';
import type { QueryComparators } from './utils/comparators';
import type { QueryFilters } from './utils/filters';
export enum QueryPreviewTabs {
queryinfo,

View File

@ -3,6 +3,8 @@ export default function isIterable(obj: unknown): boolean {
obj !== null &&
typeof obj === 'object' &&
!Array.isArray(obj) &&
typeof (obj as any)[window.Symbol.iterator] === 'function'
typeof (obj as Record<string | typeof Symbol.iterator, unknown>)[
window.Symbol.iterator
] === 'function'
);
}

View File

@ -25,6 +25,11 @@ const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
'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;
@ -282,7 +287,7 @@ export function getQueryTagsOf(
for (const [type, tagIds] of Object.entries(provided)) {
if (tagIds) {
for (const [id, queryKeys] of Object.entries(tagIds)) {
if (queryKeys.includes(queryInfo.queryKey as any)) {
if ((queryKeys as unknown[]).includes(queryInfo.queryKey)) {
const tag: RtkQueryTag = { type };
if (id !== missingTagId) {