mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-27 00:19:55 +03:00
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:
parent
9f1d718e80
commit
e84c0dcd99
|
@ -10,4 +10,4 @@ __snapshots__
|
||||||
dev
|
dev
|
||||||
.yarn/*
|
.yarn/*
|
||||||
**/.yarn/*
|
**/.yarn/*
|
||||||
demo/public/**
|
**/demo/public/**
|
|
@ -1,5 +1,2 @@
|
||||||
lib
|
lib
|
||||||
demo/public/**
|
demo/
|
||||||
demo/src/mocks/**
|
|
||||||
demo/src/build/**
|
|
||||||
demo/build/**
|
|
|
@ -1,9 +1,9 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: '../../../.eslintrc',
|
extends: '../../.eslintrc',
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.ts', '*.tsx'],
|
||||||
extends: '../../../eslintrc.ts.react.base.json',
|
extends: '../../eslintrc.ts.react.base.json',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json'],
|
||||||
|
|
|
@ -7,116 +7,116 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
|
||||||
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187';
|
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
|
||||||
const bypassHeaderName = 'x-msw-bypass';
|
const bypassHeaderName = 'x-msw-bypass'
|
||||||
const activeClientIds = new Set();
|
const activeClientIds = new Set()
|
||||||
|
|
||||||
self.addEventListener('install', function () {
|
self.addEventListener('install', function () {
|
||||||
return self.skipWaiting();
|
return self.skipWaiting()
|
||||||
});
|
})
|
||||||
|
|
||||||
self.addEventListener('activate', async function (event) {
|
self.addEventListener('activate', async function (event) {
|
||||||
return self.clients.claim();
|
return self.clients.claim()
|
||||||
});
|
})
|
||||||
|
|
||||||
self.addEventListener('message', async function (event) {
|
self.addEventListener('message', async function (event) {
|
||||||
const clientId = event.source.id;
|
const clientId = event.source.id
|
||||||
|
|
||||||
if (!clientId || !self.clients) {
|
if (!clientId || !self.clients) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await self.clients.get(clientId);
|
const client = await self.clients.get(clientId)
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll();
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
case 'KEEPALIVE_REQUEST': {
|
case 'KEEPALIVE_REQUEST': {
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'KEEPALIVE_RESPONSE',
|
type: 'KEEPALIVE_RESPONSE',
|
||||||
});
|
})
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'INTEGRITY_CHECK_REQUEST': {
|
case 'INTEGRITY_CHECK_REQUEST': {
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||||
payload: INTEGRITY_CHECKSUM,
|
payload: INTEGRITY_CHECKSUM,
|
||||||
});
|
})
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'MOCK_ACTIVATE': {
|
case 'MOCK_ACTIVATE': {
|
||||||
activeClientIds.add(clientId);
|
activeClientIds.add(clientId)
|
||||||
|
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'MOCKING_ENABLED',
|
type: 'MOCKING_ENABLED',
|
||||||
payload: true,
|
payload: true,
|
||||||
});
|
})
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'MOCK_DEACTIVATE': {
|
case 'MOCK_DEACTIVATE': {
|
||||||
activeClientIds.delete(clientId);
|
activeClientIds.delete(clientId)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'CLIENT_CLOSED': {
|
case 'CLIENT_CLOSED': {
|
||||||
activeClientIds.delete(clientId);
|
activeClientIds.delete(clientId)
|
||||||
|
|
||||||
const remainingClients = allClients.filter((client) => {
|
const remainingClients = allClients.filter((client) => {
|
||||||
return client.id !== clientId;
|
return client.id !== clientId
|
||||||
});
|
})
|
||||||
|
|
||||||
// Unregister itself when there are no more clients
|
// Unregister itself when there are no more clients
|
||||||
if (remainingClients.length === 0) {
|
if (remainingClients.length === 0) {
|
||||||
self.registration.unregister();
|
self.registration.unregister()
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Resolve the "master" client for the given event.
|
// Resolve the "master" client for the given event.
|
||||||
// Client that issues a request doesn't necessarily equal the client
|
// Client that issues a request doesn't necessarily equal the client
|
||||||
// that registered the worker. It's with the latter the worker should
|
// that registered the worker. It's with the latter the worker should
|
||||||
// communicate with during the response resolving phase.
|
// communicate with during the response resolving phase.
|
||||||
async function resolveMasterClient(event) {
|
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') {
|
if (client.frameType === 'top-level') {
|
||||||
return client;
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll();
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
return allClients
|
return allClients
|
||||||
.filter((client) => {
|
.filter((client) => {
|
||||||
// Get only those clients that are currently visible.
|
// Get only those clients that are currently visible.
|
||||||
return client.visibilityState === 'visible';
|
return client.visibilityState === 'visible'
|
||||||
})
|
})
|
||||||
.find((client) => {
|
.find((client) => {
|
||||||
// Find the client ID that's recorded in the
|
// Find the client ID that's recorded in the
|
||||||
// set of clients that have registered the worker.
|
// set of clients that have registered the worker.
|
||||||
return activeClientIds.has(client.id);
|
return activeClientIds.has(client.id)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRequest(event, requestId) {
|
async function handleRequest(event, requestId) {
|
||||||
const client = await resolveMasterClient(event);
|
const client = await resolveMasterClient(event)
|
||||||
const response = await getResponse(event, client, requestId);
|
const response = await getResponse(event, client, requestId)
|
||||||
|
|
||||||
// Send back the response clone for the "response:*" life-cycle events.
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
// Ensure MSW is active and ready to handle the message, otherwise
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
// this message will pend indefinitely.
|
// this message will pend indefinitely.
|
||||||
if (client && activeClientIds.has(client.id)) {
|
if (client && activeClientIds.has(client.id)) {
|
||||||
(async function () {
|
;(async function () {
|
||||||
const clonedResponse = response.clone();
|
const clonedResponse = response.clone()
|
||||||
sendToClient(client, {
|
sendToClient(client, {
|
||||||
type: 'RESPONSE',
|
type: 'RESPONSE',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -130,21 +130,21 @@ async function handleRequest(event, requestId) {
|
||||||
headers: serializeHeaders(clonedResponse.headers),
|
headers: serializeHeaders(clonedResponse.headers),
|
||||||
redirected: clonedResponse.redirected,
|
redirected: clonedResponse.redirected,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
})();
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResponse(event, client, requestId) {
|
async function getResponse(event, client, requestId) {
|
||||||
const { request } = event;
|
const { request } = event
|
||||||
const requestClone = request.clone();
|
const requestClone = request.clone()
|
||||||
const getOriginalResponse = () => fetch(requestClone);
|
const getOriginalResponse = () => fetch(requestClone)
|
||||||
|
|
||||||
// Bypass mocking when the request client is not active.
|
// Bypass mocking when the request client is not active.
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return getOriginalResponse();
|
return getOriginalResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass initial page load requests (i.e. static assets).
|
// 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
|
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||||
// and is not ready to handle requests.
|
// and is not ready to handle requests.
|
||||||
if (!activeClientIds.has(client.id)) {
|
if (!activeClientIds.has(client.id)) {
|
||||||
return await getOriginalResponse();
|
return await getOriginalResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass requests with the explicit bypass header
|
// Bypass requests with the explicit bypass header
|
||||||
if (requestClone.headers.get(bypassHeaderName) === 'true') {
|
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.
|
// Remove the bypass header to comply with the CORS preflight check.
|
||||||
delete cleanRequestHeaders[bypassHeaderName];
|
delete cleanRequestHeaders[bypassHeaderName]
|
||||||
|
|
||||||
const originalRequest = new Request(requestClone, {
|
const originalRequest = new Request(requestClone, {
|
||||||
headers: new Headers(cleanRequestHeaders),
|
headers: new Headers(cleanRequestHeaders),
|
||||||
});
|
})
|
||||||
|
|
||||||
return fetch(originalRequest);
|
return fetch(originalRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the request to the client-side MSW.
|
// Send the request to the client-side MSW.
|
||||||
const reqHeaders = serializeHeaders(request.headers);
|
const reqHeaders = serializeHeaders(request.headers)
|
||||||
const body = await request.text();
|
const body = await request.text()
|
||||||
|
|
||||||
const clientMessage = await sendToClient(client, {
|
const clientMessage = await sendToClient(client, {
|
||||||
type: 'REQUEST',
|
type: 'REQUEST',
|
||||||
|
@ -192,31 +192,31 @@ async function getResponse(event, client, requestId) {
|
||||||
bodyUsed: request.bodyUsed,
|
bodyUsed: request.bodyUsed,
|
||||||
keepalive: request.keepalive,
|
keepalive: request.keepalive,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
switch (clientMessage.type) {
|
switch (clientMessage.type) {
|
||||||
case 'MOCK_SUCCESS': {
|
case 'MOCK_SUCCESS': {
|
||||||
return delayPromise(
|
return delayPromise(
|
||||||
() => respondWithMock(clientMessage),
|
() => respondWithMock(clientMessage),
|
||||||
clientMessage.payload.delay
|
clientMessage.payload.delay,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'MOCK_NOT_FOUND': {
|
case 'MOCK_NOT_FOUND': {
|
||||||
return getOriginalResponse();
|
return getOriginalResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'NETWORK_ERROR': {
|
case 'NETWORK_ERROR': {
|
||||||
const { name, message } = clientMessage.payload;
|
const { name, message } = clientMessage.payload
|
||||||
const networkError = new Error(message);
|
const networkError = new Error(message)
|
||||||
networkError.name = name;
|
networkError.name = name
|
||||||
|
|
||||||
// Rejecting a request Promise emulates a network error.
|
// Rejecting a request Promise emulates a network error.
|
||||||
throw networkError;
|
throw networkError
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'INTERNAL_ERROR': {
|
case 'INTERNAL_ERROR': {
|
||||||
const parsedBody = JSON.parse(clientMessage.payload.body);
|
const parsedBody = JSON.parse(clientMessage.payload.body)
|
||||||
|
|
||||||
console.error(
|
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\
|
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
|
||||||
`,
|
`,
|
||||||
request.method,
|
request.method,
|
||||||
request.url
|
request.url,
|
||||||
);
|
)
|
||||||
|
|
||||||
return respondWithMock(clientMessage);
|
return respondWithMock(clientMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getOriginalResponse();
|
return getOriginalResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('fetch', function (event) {
|
self.addEventListener('fetch', function (event) {
|
||||||
const { request } = event;
|
const { request } = event
|
||||||
|
|
||||||
// Bypass navigation requests.
|
// Bypass navigation requests.
|
||||||
if (request.mode === 'navigate') {
|
if (request.mode === 'navigate') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening the DevTools triggers the "only-if-cached" request
|
// Opening the DevTools triggers the "only-if-cached" request
|
||||||
// that cannot be handled by the worker. Bypass such requests.
|
// that cannot be handled by the worker. Bypass such requests.
|
||||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass all requests when there are no active clients.
|
// Bypass all requests when there are no active clients.
|
||||||
// Prevents the self-unregistered worked from handling requests
|
// Prevents the self-unregistered worked from handling requests
|
||||||
// after it's been deleted (still remains active until the next reload).
|
// after it's been deleted (still remains active until the next reload).
|
||||||
if (activeClientIds.size === 0) {
|
if (activeClientIds.size === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = uuidv4();
|
const requestId = uuidv4()
|
||||||
|
|
||||||
return event.respondWith(
|
return event.respondWith(
|
||||||
handleRequest(event, requestId).catch((error) => {
|
handleRequest(event, requestId).catch((error) => {
|
||||||
|
@ -268,55 +268,55 @@ self.addEventListener('fetch', function (event) {
|
||||||
'[MSW] Failed to mock a "%s" request to "%s": %s',
|
'[MSW] Failed to mock a "%s" request to "%s": %s',
|
||||||
request.method,
|
request.method,
|
||||||
request.url,
|
request.url,
|
||||||
error
|
error,
|
||||||
);
|
)
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
function serializeHeaders(headers) {
|
function serializeHeaders(headers) {
|
||||||
const reqHeaders = {};
|
const reqHeaders = {}
|
||||||
headers.forEach((value, name) => {
|
headers.forEach((value, name) => {
|
||||||
reqHeaders[name] = reqHeaders[name]
|
reqHeaders[name] = reqHeaders[name]
|
||||||
? [].concat(reqHeaders[name]).concat(value)
|
? [].concat(reqHeaders[name]).concat(value)
|
||||||
: value;
|
: value
|
||||||
});
|
})
|
||||||
return reqHeaders;
|
return reqHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToClient(client, message) {
|
function sendToClient(client, message) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const channel = new MessageChannel();
|
const channel = new MessageChannel()
|
||||||
|
|
||||||
channel.port1.onmessage = (event) => {
|
channel.port1.onmessage = (event) => {
|
||||||
if (event.data && event.data.error) {
|
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) {
|
function delayPromise(cb, duration) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => resolve(cb()), duration);
|
setTimeout(() => resolve(cb()), duration)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function respondWithMock(clientMessage) {
|
function respondWithMock(clientMessage) {
|
||||||
return new Response(clientMessage.payload.body, {
|
return new Response(clientMessage.payload.body, {
|
||||||
...clientMessage.payload,
|
...clientMessage.payload,
|
||||||
headers: clientMessage.payload.headers,
|
headers: clientMessage.payload.headers,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function uuidv4() {
|
function uuidv4() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
const r = (Math.random() * 16) | 0;
|
const r = (Math.random() * 16) | 0
|
||||||
const v = c == 'x' ? r : (r & 0x3) | 0x8;
|
const v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||||
return v.toString(16);
|
return v.toString(16)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ const PostJsonDetail = ({ id }: { id: string }) => {
|
||||||
|
|
||||||
export const PostDetail = () => {
|
export const PostDetail = () => {
|
||||||
const { id } = useParams<{ id: any }>();
|
const { id } = useParams<{ id: any }>();
|
||||||
const { push } = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -137,7 +137,9 @@ export const PostDetail = () => {
|
||||||
{isUpdating ? 'Updating...' : 'Edit'}
|
{isUpdating ? 'Updating...' : 'Edit'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => deletePost(id).then(() => push('/posts'))}
|
onClick={() =>
|
||||||
|
deletePost(id).then(() => history.push('/posts'))
|
||||||
|
}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
>
|
>
|
||||||
|
|
|
@ -82,7 +82,7 @@ const AddPost = () => {
|
||||||
|
|
||||||
const PostList = () => {
|
const PostList = () => {
|
||||||
const { data: posts, isLoading } = useGetPostsQuery();
|
const { data: posts, isLoading } = useGetPostsQuery();
|
||||||
const { push } = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div>Loading</div>;
|
return <div>Loading</div>;
|
||||||
|
@ -95,7 +95,7 @@ const PostList = () => {
|
||||||
return (
|
return (
|
||||||
<List spacing={3}>
|
<List spacing={3}>
|
||||||
{posts.map(({ id, name }) => (
|
{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}
|
<ListIcon as={MdBook} color="green.500" /> {name}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
export function NoRtkQueryApi(): ReactNode {
|
export function NoRtkQueryApi(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<StyleUtilsContext.Consumer>
|
<StyleUtilsContext.Consumer>
|
||||||
{({ styling }) => (
|
{({ styling }) => (
|
||||||
<div {...styling('noApiFound')}>
|
<div {...styling('noApiFound')}>
|
||||||
No rtk-query api found.<br/>Make sure to follow{' '}
|
No rtk-query api found.
|
||||||
|
<br />
|
||||||
|
Make sure to follow{' '}
|
||||||
<a
|
<a
|
||||||
href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage"
|
href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { SelectOption } from '../types';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
|
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
|
||||||
import { QueryFilters, filterQueryOptions } from '../utils/filters';
|
import { QueryFilters, filterQueryOptions } from '../utils/filters';
|
||||||
|
import { SortOrderButtons } from './SortOrderButtons';
|
||||||
|
|
||||||
export interface QueryFormProps {
|
export interface QueryFormProps {
|
||||||
values: QueryFormValues;
|
values: QueryFormValues;
|
||||||
|
@ -16,8 +17,6 @@ interface QueryFormState {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ascId = 'rtk-query-rb-asc';
|
|
||||||
const descId = 'rtk-query-rb-desc';
|
|
||||||
const selectId = 'rtk-query-comp-select';
|
const selectId = 'rtk-query-comp-select';
|
||||||
const searchId = 'rtk-query-search-query';
|
const searchId = 'rtk-query-search-query';
|
||||||
const filterSelectId = 'rtk-query-search-query-select';
|
const filterSelectId = 'rtk-query-search-query-select';
|
||||||
|
@ -41,19 +40,8 @@ export class QueryForm extends React.PureComponent<
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => {
|
handleButtonGroupClick = (isAsc: boolean): void => {
|
||||||
const {
|
this.props.onFormValuesChange({ isAscendingQueryComparatorOrder: isAsc });
|
||||||
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 });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSelectComparatorChange = (
|
handleSelectComparatorChange = (
|
||||||
|
@ -111,8 +99,6 @@ export class QueryForm extends React.PureComponent<
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isDesc = !isAsc;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyleUtilsContext.Consumer>
|
<StyleUtilsContext.Consumer>
|
||||||
{({ styling, base16Theme }) => {
|
{({ styling, base16Theme }) => {
|
||||||
|
@ -170,37 +156,10 @@ export class QueryForm extends React.PureComponent<
|
||||||
options={sortQueryOptions}
|
options={sortQueryOptions}
|
||||||
onChange={this.handleSelectComparatorChange}
|
onChange={this.handleSelectComparatorChange}
|
||||||
/>
|
/>
|
||||||
<div
|
<SortOrderButtons
|
||||||
tabIndex={0}
|
isAsc={isAsc}
|
||||||
role="radiogroup"
|
onChange={this.handleButtonGroupClick}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,37 +1,33 @@
|
||||||
import React, { Component, createRef, ReactNode } from 'react';
|
import React, { PureComponent, createRef, ReactNode } from 'react';
|
||||||
import { AnyAction, Dispatch, Action } from 'redux';
|
import type { AnyAction, Dispatch, Action } from '@reduxjs/toolkit';
|
||||||
import { LiftedAction, LiftedState } from '@redux-devtools/core';
|
import type { LiftedAction, LiftedState } from '@redux-devtools/core';
|
||||||
import * as themes from 'redux-devtools-themes';
|
|
||||||
import { Base16Theme } from 'react-base16-styling';
|
|
||||||
import {
|
import {
|
||||||
QueryFormValues,
|
QueryFormValues,
|
||||||
QueryInfo,
|
QueryInfo,
|
||||||
QueryPreviewTabs,
|
QueryPreviewTabs,
|
||||||
RtkQueryMonitorState,
|
RtkQueryMonitorState,
|
||||||
StyleUtils,
|
StyleUtils,
|
||||||
} from './types';
|
SelectorsSource,
|
||||||
import { createInspectorSelectors, computeSelectorSource } from './selectors';
|
} from '../types';
|
||||||
|
import { createInspectorSelectors, computeSelectorSource } from '../selectors';
|
||||||
import {
|
import {
|
||||||
changeQueryFormValues,
|
changeQueryFormValues,
|
||||||
selectedPreviewTab,
|
selectedPreviewTab,
|
||||||
selectQueryKey,
|
selectQueryKey,
|
||||||
} from './reducers';
|
} from '../reducers';
|
||||||
import { QueryList } from './components/QueryList';
|
import { QueryList } from '../components/QueryList';
|
||||||
import { QueryForm } from './components/QueryForm';
|
import { QueryForm } from '../components/QueryForm';
|
||||||
import { QueryPreview } from './components/QueryPreview';
|
import { QueryPreview } from '../components/QueryPreview';
|
||||||
import { getApiStateOf, getQuerySubscriptionsOf } from './utils/rtk-query';
|
import { getApiStateOf, getQuerySubscriptionsOf } from '../utils/rtk-query';
|
||||||
|
|
||||||
type SelectorsSource<S> = {
|
type ForwardedMonitorProps<S, A extends Action<unknown>> = Pick<
|
||||||
userState: S | null;
|
LiftedState<S, A, RtkQueryMonitorState>,
|
||||||
monitorState: RtkQueryMonitorState;
|
'monitorState' | 'currentStateIndex' | 'computedStates'
|
||||||
};
|
>;
|
||||||
|
|
||||||
export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
|
export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
|
||||||
extends LiftedState<S, A, RtkQueryMonitorState> {
|
extends ForwardedMonitorProps<S, A> {
|
||||||
dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>;
|
dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>;
|
||||||
theme: keyof typeof themes | Base16Theme;
|
|
||||||
invertTheme: boolean;
|
|
||||||
state: S | null;
|
|
||||||
styleUtils: StyleUtils;
|
styleUtils: StyleUtils;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,13 +36,13 @@ type RtkQueryInspectorState<S> = {
|
||||||
isWideLayout: boolean;
|
isWideLayout: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
RtkQueryInspectorProps<S, A>,
|
RtkQueryInspectorProps<S, A>,
|
||||||
RtkQueryInspectorState<S>
|
RtkQueryInspectorState<S>
|
||||||
> {
|
> {
|
||||||
inspectorRef = createRef<HTMLDivElement>();
|
inspectorRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
isWideIntervalRef: number | NodeJS.Timeout | null = null;
|
isWideIntervalRef: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor(props: RtkQueryInspectorProps<S, A>) {
|
constructor(props: RtkQueryInspectorProps<S, A>) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -98,7 +94,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
if (this.isWideIntervalRef) {
|
if (this.isWideIntervalRef) {
|
||||||
clearTimeout(this.isWideIntervalRef as any);
|
clearTimeout(this.isWideIntervalRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Action } from 'redux';
|
import { Action, AnyAction } from 'redux';
|
||||||
import RtkQueryInspector from './RtkQueryInspector';
|
import RtkQueryInspector from './RtkQueryInspector';
|
||||||
import { reducer } from './reducers';
|
import { reducer } from '../reducers';
|
||||||
import {
|
import {
|
||||||
ExternalProps,
|
ExternalProps,
|
||||||
RtkQueryMonitorProps,
|
RtkQueryMonitorProps,
|
||||||
RtkQueryMonitorState,
|
RtkQueryMonitorState,
|
||||||
StyleUtils,
|
StyleUtils,
|
||||||
} from './types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
createThemeState,
|
createThemeState,
|
||||||
StyleUtilsContext,
|
StyleUtilsContext,
|
||||||
} from './styles/createStylingFromTheme';
|
} from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
interface DefaultProps {
|
interface DefaultProps {
|
||||||
theme: string;
|
theme: string;
|
||||||
|
@ -41,7 +41,7 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
|
||||||
invertTheme: PropTypes.bool,
|
invertTheme: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps: DefaultProps = {
|
||||||
theme: 'nicinabox',
|
theme: 'nicinabox',
|
||||||
invertTheme: false,
|
invertTheme: false,
|
||||||
};
|
};
|
||||||
|
@ -55,17 +55,16 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { currentStateIndex, computedStates, monitorState, dispatch } =
|
||||||
styleUtils: { base16Theme },
|
this.props;
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const RtkQueryInspectorAsAny = RtkQueryInspector as any;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyleUtilsContext.Provider value={this.state.styleUtils}>
|
<StyleUtilsContext.Provider value={this.state.styleUtils}>
|
||||||
<RtkQueryInspectorAsAny
|
<RtkQueryInspector<S, AnyAction>
|
||||||
{...this.props}
|
computedStates={computedStates}
|
||||||
theme={base16Theme}
|
currentStateIndex={currentStateIndex}
|
||||||
|
monitorState={monitorState}
|
||||||
|
dispatch={dispatch}
|
||||||
styleUtils={this.state.styleUtils}
|
styleUtils={this.state.styleUtils}
|
||||||
/>
|
/>
|
||||||
</StyleUtilsContext.Provider>
|
</StyleUtilsContext.Provider>
|
|
@ -1,2 +1,2 @@
|
||||||
export { default } from './RtkQueryMonitor';
|
export { default } from './containers/RtkQueryMonitor';
|
||||||
export { ExternalProps } from './types';
|
export { ExternalProps } from './types';
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Action, AnyAction } from 'redux';
|
import { Action, AnyAction } from 'redux';
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { RtkQueryInspectorProps } from './RtkQueryInspector';
|
|
||||||
import {
|
import {
|
||||||
QueryInfo,
|
QueryInfo,
|
||||||
RtkQueryMonitorState,
|
RtkQueryMonitorState,
|
||||||
QueryFormValues,
|
QueryFormValues,
|
||||||
|
RtkQueryMonitorProps,
|
||||||
QueryPreviewTabs,
|
QueryPreviewTabs,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { QueryComparators } from './utils/comparators';
|
import { QueryComparators } from './utils/comparators';
|
||||||
|
@ -53,7 +53,7 @@ const monitorSlice = createSlice({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function reducer<S, A extends Action<unknown>>(
|
export function reducer<S, A extends Action<unknown>>(
|
||||||
props: RtkQueryInspectorProps<S, A>,
|
props: RtkQueryMonitorProps<S, A>,
|
||||||
state: RtkQueryMonitorState | undefined,
|
state: RtkQueryMonitorState | undefined,
|
||||||
action: AnyAction
|
action: AnyAction
|
||||||
): RtkQueryMonitorState {
|
): RtkQueryMonitorState {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
|
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
|
||||||
import { RtkQueryInspectorProps } from './RtkQueryInspector';
|
import { RtkQueryInspectorProps } from './containers/RtkQueryInspector';
|
||||||
import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types';
|
import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types';
|
||||||
import { Comparator, queryComparators } from './utils/comparators';
|
import { Comparator, queryComparators } from './utils/comparators';
|
||||||
import { FilterList, queryListFilters } from './utils/filters';
|
import { FilterList, queryListFilters } from './utils/filters';
|
||||||
|
|
|
@ -14,7 +14,8 @@ import { createContext } from 'react';
|
||||||
|
|
||||||
jss.setup(preset());
|
jss.setup(preset());
|
||||||
|
|
||||||
export const colorMap = (theme: reduxThemes.Base16Theme) => ({
|
export const colorMap = (theme: reduxThemes.Base16Theme) =>
|
||||||
|
({
|
||||||
TEXT_COLOR: theme.base06,
|
TEXT_COLOR: theme.base06,
|
||||||
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
|
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
|
||||||
BACKGROUND_COLOR: theme.base00,
|
BACKGROUND_COLOR: theme.base00,
|
||||||
|
@ -38,7 +39,7 @@ export const colorMap = (theme: reduxThemes.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,
|
||||||
});
|
} as const);
|
||||||
|
|
||||||
type Color = keyof ReturnType<typeof colorMap>;
|
type Color = keyof ReturnType<typeof colorMap>;
|
||||||
type ColorMap = {
|
type ColorMap = {
|
||||||
|
|
|
@ -5,11 +5,11 @@ import isIterable from '../utils/isIterable';
|
||||||
|
|
||||||
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
||||||
|
|
||||||
function isImmutable(value: any) {
|
function isImmutable(value: unknown) {
|
||||||
return isKeyed(value) || isIndexed(value) || isCollection(value);
|
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)) {
|
if (diff && Array.isArray(val)) {
|
||||||
val = val[val.length === 2 ? 1 : 0];
|
val = val[val.length === 2 ? 1 : 0];
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,9 @@ function getShortTypeString(val: any, diff: boolean | undefined) {
|
||||||
} else if (val === undefined) {
|
} else if (val === undefined) {
|
||||||
return 'undef';
|
return 'undef';
|
||||||
} else if (typeof val === 'object') {
|
} 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') {
|
} else if (typeof val === 'function') {
|
||||||
return 'fn';
|
return 'fn';
|
||||||
} else if (typeof val === 'string') {
|
} else if (typeof val === 'string') {
|
||||||
|
|
|
@ -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 type { createApi, QueryStatus } from '@reduxjs/toolkit/query';
|
||||||
import { ComponentType, Dispatch } from 'react';
|
import type { Action, Dispatch } from '@reduxjs/toolkit';
|
||||||
import { Base16Theme, StylingFunction } from 'react-base16-styling';
|
import type { ComponentType } from 'react';
|
||||||
import { Action } from 'redux';
|
import type { Base16Theme, StylingFunction } from 'react-base16-styling';
|
||||||
import * as themes from 'redux-devtools-themes';
|
import type * as themes from 'redux-devtools-themes';
|
||||||
import { QueryComparators } from './utils/comparators';
|
import type { QueryComparators } from './utils/comparators';
|
||||||
import { QueryFilters } from './utils/filters';
|
import type { QueryFilters } from './utils/filters';
|
||||||
|
|
||||||
export enum QueryPreviewTabs {
|
export enum QueryPreviewTabs {
|
||||||
queryinfo,
|
queryinfo,
|
||||||
|
|
|
@ -3,6 +3,8 @@ export default function isIterable(obj: unknown): boolean {
|
||||||
obj !== null &&
|
obj !== null &&
|
||||||
typeof obj === 'object' &&
|
typeof obj === 'object' &&
|
||||||
!Array.isArray(obj) &&
|
!Array.isArray(obj) &&
|
||||||
typeof (obj as any)[window.Symbol.iterator] === 'function'
|
typeof (obj as Record<string | typeof Symbol.iterator, unknown>)[
|
||||||
|
window.Symbol.iterator
|
||||||
|
] === 'function'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,11 @@ const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
||||||
'subscriptions',
|
'subscriptions',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard used to select apis from the user store state.
|
||||||
|
* @param val
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
export function isApiSlice(val: unknown): val is RtkQueryApiState {
|
export function isApiSlice(val: unknown): val is RtkQueryApiState {
|
||||||
if (!isPlainObject(val)) {
|
if (!isPlainObject(val)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -282,7 +287,7 @@ export function getQueryTagsOf(
|
||||||
for (const [type, tagIds] of Object.entries(provided)) {
|
for (const [type, tagIds] of Object.entries(provided)) {
|
||||||
if (tagIds) {
|
if (tagIds) {
|
||||||
for (const [id, queryKeys] of Object.entries(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 };
|
const tag: RtkQueryTag = { type };
|
||||||
|
|
||||||
if (id !== missingTagId) {
|
if (id !== missingTagId) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user