Add object search and prepare types

This commit is contained in:
Patrick van Beek 2023-06-16 17:24:10 +02:00 committed by Zsofia Vagi
parent 27da2edc1a
commit 2097b49ffe
6 changed files with 169 additions and 19 deletions

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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 };

View File

@ -47,6 +47,8 @@ export interface CommonExternalProps {
labelRenderer: LabelRenderer;
valueRenderer: ValueRenderer;
shouldExpandNodeInitially: ShouldExpandNodeInitially;
searchResultPath: KeyPath;
isSearchInProgress: boolean;
hideRoot: boolean;
getItemString: GetItemString;
postprocessValue: PostprocessValue;

View File

@ -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 });
});
}

View File

@ -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 {};