diff --git a/packages/react-json-tree/src/ItemRange.tsx b/packages/react-json-tree/src/ItemRange.tsx index 52abad4a..25160db5 100644 --- a/packages/react-json-tree/src/ItemRange.tsx +++ b/packages/react-json-tree/src/ItemRange.tsx @@ -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(false); + const [userExpanded, setUserExpanded] = useState(false); const handleClick = useCallback(() => { - setExpanded(!expanded); - }, [expanded]); + setUserExpanded(!userExpanded); + }, [userExpanded]); + + const expanded = + userExpanded || + containsSearchResult(keyPath, from, to, searchResultPath, level); return expanded ? (
@@ -37,3 +50,15 @@ export default function ItemRange(props: Props) {
); } + +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; +}; diff --git a/packages/react-json-tree/src/JSONNestedNode.tsx b/packages/react-json-tree/src/JSONNestedNode.tsx index e62d6532..6e197d02 100644 --- a/packages/react-json-tree/src/JSONNestedNode.tsx +++ b/packages/react-json-tree/src/JSONNestedNode.tsx @@ -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( + const [userExpanded, setUserExpanded] = useState( // 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) { ); } + +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; +}; diff --git a/packages/react-json-tree/src/index.tsx b/packages/react-json-tree/src/index.tsx index 5fc23dae..23065be6 100644 --- a/packages/react-json-tree/src/index.tsx +++ b/packages/react-json-tree/src/index.tsx @@ -4,10 +4,10 @@ // port by Daniele Zannotti http://www.github.com/dzannotti 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 }; diff --git a/packages/react-json-tree/src/types.ts b/packages/react-json-tree/src/types.ts index e0461261..e687d84a 100644 --- a/packages/react-json-tree/src/types.ts +++ b/packages/react-json-tree/src/types.ts @@ -47,6 +47,8 @@ export interface CommonExternalProps { labelRenderer: LabelRenderer; valueRenderer: ValueRenderer; shouldExpandNodeInitially: ShouldExpandNodeInitially; + searchResultPath: KeyPath; + isSearchInProgress: boolean; hideRoot: boolean; getItemString: GetItemString; postprocessValue: PostprocessValue; diff --git a/packages/redux-devtools-inspector-monitor/src/utils/objectSearch.ts b/packages/redux-devtools-inspector-monitor/src/utils/objectSearch.ts new file mode 100644 index 00000000..2fd05109 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/objectSearch.ts @@ -0,0 +1,18 @@ +import { SearchQuery } from '../searchPanel/SearchPanel'; +import { Value } from './searchWorker'; + +export function searchInObject( + objectToSearch: Value, + query: SearchQuery +): Promise { + return new Promise((resolve) => { + const worker = new Worker(new URL('./searchWorker.js', import.meta.url)); + + worker.onmessage = (event: MessageEvent) => { + resolve(event.data); + worker.terminate(); + }; + + worker.postMessage({ objectToSearch, query }); + }); +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/searchWorker.ts b/packages/redux-devtools-inspector-monitor/src/utils/searchWorker.ts new file mode 100644 index 00000000..55ebc134 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/searchWorker.ts @@ -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 {};