mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-22 06:00:07 +03:00
Add object search and prepare types
This commit is contained in:
parent
27da2edc1a
commit
2097b49ffe
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import JSONArrow from './JSONArrow';
|
||||
import type { CircularCache, CommonInternalProps } from './types';
|
||||
import type { CircularCache, CommonInternalProps, KeyPath } from './types';
|
||||
|
||||
interface Props extends CommonInternalProps {
|
||||
data: unknown;
|
||||
|
@ -13,12 +13,25 @@ interface Props extends CommonInternalProps {
|
|||
}
|
||||
|
||||
export default function ItemRange(props: Props) {
|
||||
const { styling, from, to, renderChildNodes, nodeType } = props;
|
||||
const {
|
||||
styling,
|
||||
from,
|
||||
to,
|
||||
renderChildNodes,
|
||||
nodeType,
|
||||
keyPath,
|
||||
searchResultPath,
|
||||
level,
|
||||
} = props;
|
||||
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [userExpanded, setUserExpanded] = useState<boolean>(false);
|
||||
const handleClick = useCallback(() => {
|
||||
setExpanded(!expanded);
|
||||
}, [expanded]);
|
||||
setUserExpanded(!userExpanded);
|
||||
}, [userExpanded]);
|
||||
|
||||
const expanded =
|
||||
userExpanded ||
|
||||
containsSearchResult(keyPath, from, to, searchResultPath, level);
|
||||
|
||||
return expanded ? (
|
||||
<div {...styling('itemRange', expanded)}>
|
||||
|
@ -37,3 +50,15 @@ export default function ItemRange(props: Props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containsSearchResult = (
|
||||
ownKeyPath: KeyPath,
|
||||
from: number,
|
||||
to: number,
|
||||
resultKeyPath: KeyPath,
|
||||
level: number
|
||||
): boolean => {
|
||||
const searchLevel = level > 1 ? level - 1 : level;
|
||||
const nextKey = Number(resultKeyPath[searchLevel]);
|
||||
return !isNaN(nextKey) && nextKey >= from && nextKey <= to;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import JSONArrow from './JSONArrow';
|
||||
import getCollectionEntries from './getCollectionEntries';
|
||||
import JSONNode from './JSONNode';
|
||||
import ItemRange from './ItemRange';
|
||||
import type { CircularCache, CommonInternalProps } from './types';
|
||||
import JSONArrow from './JSONArrow';
|
||||
import JSONNode from './JSONNode';
|
||||
import getCollectionEntries from './getCollectionEntries';
|
||||
import type { CircularCache, CommonInternalProps, KeyPath } from './types';
|
||||
|
||||
/**
|
||||
* Renders nested values (eg. objects, arrays, lists, etc.)
|
||||
|
@ -110,17 +110,21 @@ export default function JSONNestedNode(props: Props) {
|
|||
nodeType,
|
||||
nodeTypeIndicator,
|
||||
shouldExpandNodeInitially,
|
||||
searchResultPath,
|
||||
styling,
|
||||
} = props;
|
||||
|
||||
const [expanded, setExpanded] = useState<boolean>(
|
||||
const [userExpanded, setUserExpanded] = useState<boolean>(
|
||||
// calculate individual node expansion if necessary
|
||||
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level),
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (expandable) setExpanded(!expanded);
|
||||
}, [expandable, expanded]);
|
||||
if (expandable) setUserExpanded(!userExpanded);
|
||||
}, [expandable, userExpanded]);
|
||||
|
||||
const expanded =
|
||||
userExpanded || containsSearchResult(keyPath, searchResultPath, level);
|
||||
|
||||
const renderedChildren =
|
||||
expanded || (hideRoot && level === 0)
|
||||
|
@ -175,3 +179,15 @@ export default function JSONNestedNode(props: Props) {
|
|||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const containsSearchResult = (
|
||||
ownKeyPath: KeyPath,
|
||||
resultKeyPath: KeyPath,
|
||||
level: number
|
||||
): boolean => {
|
||||
const searchLevel = level > 0 ? level - 1 : level;
|
||||
const currKey = [...ownKeyPath].reverse()[searchLevel];
|
||||
return resultKeyPath && currKey !== undefined
|
||||
? resultKeyPath[searchLevel] === currKey.toString()
|
||||
: false;
|
||||
};
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { StylingValue, Theme } from 'react-base16-styling';
|
||||
import { invertTheme } from 'react-base16-styling';
|
||||
import JSONNode from './JSONNode';
|
||||
import createStylingFromTheme from './createStylingFromTheme';
|
||||
import { invertTheme } from 'react-base16-styling';
|
||||
import type { StylingValue, Theme } from 'react-base16-styling';
|
||||
import type {
|
||||
CommonExternalProps,
|
||||
GetItemString,
|
||||
|
@ -41,6 +41,8 @@ export function JSONTree({
|
|||
labelRenderer = defaultLabelRenderer,
|
||||
valueRenderer = identity,
|
||||
shouldExpandNodeInitially = expandRootNode,
|
||||
isSearchInProgress = false,
|
||||
searchResultPath = [],
|
||||
hideRoot = false,
|
||||
getItemString = defaultItemString,
|
||||
postprocessValue = identity,
|
||||
|
@ -64,6 +66,8 @@ export function JSONTree({
|
|||
labelRenderer={labelRenderer}
|
||||
valueRenderer={valueRenderer}
|
||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||
searchResultPath={searchResultPath}
|
||||
isSearchInProgress={isSearchInProgress}
|
||||
hideRoot={hideRoot}
|
||||
getItemString={getItemString}
|
||||
postprocessValue={postprocessValue}
|
||||
|
@ -75,16 +79,16 @@ export function JSONTree({
|
|||
}
|
||||
|
||||
export type {
|
||||
CommonExternalProps,
|
||||
GetItemString,
|
||||
IsCustomNode,
|
||||
Key,
|
||||
KeyPath,
|
||||
GetItemString,
|
||||
LabelRenderer,
|
||||
ValueRenderer,
|
||||
ShouldExpandNodeInitially,
|
||||
PostprocessValue,
|
||||
IsCustomNode,
|
||||
ShouldExpandNodeInitially,
|
||||
SortObjectKeys,
|
||||
Styling,
|
||||
CommonExternalProps,
|
||||
ValueRenderer,
|
||||
} from './types';
|
||||
export type { StylingValue };
|
||||
|
|
|
@ -47,6 +47,8 @@ export interface CommonExternalProps {
|
|||
labelRenderer: LabelRenderer;
|
||||
valueRenderer: ValueRenderer;
|
||||
shouldExpandNodeInitially: ShouldExpandNodeInitially;
|
||||
searchResultPath: KeyPath;
|
||||
isSearchInProgress: boolean;
|
||||
hideRoot: boolean;
|
||||
getItemString: GetItemString;
|
||||
postprocessValue: PostprocessValue;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { SearchQuery } from '../searchPanel/SearchPanel';
|
||||
import { Value } from './searchWorker';
|
||||
|
||||
export function searchInObject(
|
||||
objectToSearch: Value,
|
||||
query: SearchQuery
|
||||
): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
const worker = new Worker(new URL('./searchWorker.js', import.meta.url));
|
||||
|
||||
worker.onmessage = (event: MessageEvent<string[]>) => {
|
||||
resolve(event.data);
|
||||
worker.terminate();
|
||||
};
|
||||
|
||||
worker.postMessage({ objectToSearch, query });
|
||||
});
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { SearchQuery } from '../searchPanel/SearchPanel';
|
||||
|
||||
type PrimitiveValue = string | number | boolean | undefined | null;
|
||||
export type Value =
|
||||
| PrimitiveValue
|
||||
| Value[]
|
||||
| { [key: string | number]: Value };
|
||||
|
||||
function getKeys(object: Value): string[] {
|
||||
if (object === undefined || object === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (['string', 'number', 'boolean'].includes(typeof object)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return [...object.keys()].map((el) => el.toString());
|
||||
}
|
||||
|
||||
return Object.keys(object);
|
||||
}
|
||||
|
||||
function getKeyPaths(object: Value): string[] {
|
||||
const keys = getKeys(object) as (keyof object)[];
|
||||
const appendKey = (path: string, branch: string | number): string =>
|
||||
path ? `${path}.${branch}` : `${branch}`;
|
||||
|
||||
return keys.flatMap((key) => [
|
||||
key,
|
||||
...getKeyPaths(object?.[key]).map((subKey) => appendKey(key, subKey)),
|
||||
]);
|
||||
}
|
||||
|
||||
const isMatchable = (value: Value): boolean => {
|
||||
return (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
['string', 'number', 'boolean'].includes(typeof value)
|
||||
);
|
||||
};
|
||||
|
||||
function doSearch(objectToSearch: Value, query: SearchQuery): string[] {
|
||||
const {
|
||||
queryText,
|
||||
location: { keys: matchKeys, values: matchValues },
|
||||
} = query;
|
||||
|
||||
if (!matchKeys && !matchValues) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keyPaths = getKeyPaths(objectToSearch);
|
||||
const getValueFromKeyPath = (obj: Value, path: string): Value => {
|
||||
return path
|
||||
.split('.')
|
||||
.reduce(
|
||||
(intermediateObj, key) => intermediateObj?.[key as keyof object],
|
||||
obj
|
||||
);
|
||||
};
|
||||
const match = (value: Value, searchText: string): boolean => {
|
||||
return isMatchable(value) && String(value).includes(searchText);
|
||||
};
|
||||
|
||||
return keyPaths.filter((keyPath) => {
|
||||
const valuesToCheck = [];
|
||||
matchKeys && valuesToCheck.push(keyPath.split('.').splice(-1)[0]);
|
||||
matchValues &&
|
||||
valuesToCheck.push(getValueFromKeyPath(objectToSearch, keyPath));
|
||||
|
||||
return valuesToCheck.some((value) => match(value, queryText));
|
||||
});
|
||||
}
|
||||
|
||||
self.onmessage = (
|
||||
event: MessageEvent<{ objectToSearch: Value; query: SearchQuery }>
|
||||
) => {
|
||||
const { objectToSearch, query } = event.data;
|
||||
const result = doSearch(objectToSearch, query);
|
||||
self.postMessage(result);
|
||||
};
|
||||
|
||||
export {};
|
Loading…
Reference in New Issue
Block a user