WIP implementing search
This commit is contained in:
@@ -1,21 +1,30 @@
|
||||
import { ExclamationIcon, SearchIcon } from "@heroicons/react/outline";
|
||||
import { SearchIcon } from "@heroicons/react/outline";
|
||||
import { ShortcutIcon } from "./Icons/ShortcutIcon";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { Dialog, DialogTrigger, DialogContent } from "./UI/Dialog";
|
||||
import { EscapeKeyIcon } from "./Icons/EscapeKeyIcon";
|
||||
import { ArrowKeysUpDownIcon } from "./Icons/ArrowKeysUpDownIcon";
|
||||
import { LoadingIcon } from "./Icons/LoadingIcon";
|
||||
import { SearchItem } from "./SearchItem";
|
||||
|
||||
import classnames from "~/utilities/classnames";
|
||||
import { SearchPalette } from "./SearchPalette";
|
||||
import { useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useJsonColumnViewAPI } from "~/hooks/useJsonColumnView";
|
||||
|
||||
export type SearchBarProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function SearchBar() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
useHotkeys(
|
||||
"cmd+k",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
},
|
||||
[setIsOpen]
|
||||
);
|
||||
|
||||
export function SearchBar({ className }: SearchBarProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Dialog open={isOpen}>
|
||||
<DialogTrigger onClick={() => setIsOpen(true)}>
|
||||
<div className="flex justify-between items-center group w-44 rounded-sm bg-slate-300 transition hover:bg-slate-400 hover:bg-opacity-50 dark:bg-slate-800 dark:text-slate-400 hover:cursor-pointer hover:dark:bg-slate-700 hover:dark:bg-opacity-70">
|
||||
<div className="flex items-center pl-1">
|
||||
<SearchIcon className="w-4 h-4 mr-1" />
|
||||
@@ -40,73 +49,14 @@ export function SearchBar({ className }: SearchBarProps) {
|
||||
"bg-white border-[1px] dark:border-slate-700 dark:bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<JamesCommandPalette />
|
||||
<SearchPalette
|
||||
onClose={() => setIsOpen(false)}
|
||||
onSelect={(entry) => {
|
||||
setIsOpen(false);
|
||||
goToNodeId(entry.path, "search");
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function JamesCommandPalette() {
|
||||
return (
|
||||
<>
|
||||
<div className="search-container max-h-[60vh] px-4 pt-4 overflow-hidden">
|
||||
<label className="relative text-gray-400 focus-within:text-gray-600 block">
|
||||
<SearchIcon className="absolute w-7 h-7 top-1/2 transform -translate-y-1/2 left-3 text-white pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search the JSON…"
|
||||
className="search-field w-full pl-12 pr-4 py-4 rounded-sm text-white text-2xl caret-indigo-700 bg-slate-900 border-indigo-700 focus:outline-none focus:ring focus:ring-indigo-700"
|
||||
/>
|
||||
</label>
|
||||
<div className="search-content flex flex-col mt-4 mb-2">
|
||||
<div className="results flex">
|
||||
<div className="results-loading flex">
|
||||
<LoadingIcon className="animate-spin h-5 w-5 mr-1"></LoadingIcon>
|
||||
<Body className="text-slate-400">Loading…</Body>
|
||||
</div>
|
||||
<div className="results-returned">
|
||||
<Body className="text-slate-400">35 results</Body>
|
||||
</div>
|
||||
<div className="results-none flex">
|
||||
<ExclamationIcon className="h-5 w-5 mr-1 text-white"></ExclamationIcon>
|
||||
<Body className="text-slate-400">
|
||||
No results for "sdgiuhnkjdfg"
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="search-items w-full max-h-[inherit] pb-24 overflow-y-auto">
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
<SearchItem></SearchItem>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="footer flex items-center w-full gap-4 px-3 py-2 border-t-[1px] border-slate-700 rounded-br-lg rounded-bl-lg bg-slate-900">
|
||||
<div className="flex items-center gap-1">
|
||||
<ShortcutIcon className="w-4 h-4 text-sm text-slate-900 bg-slate-300 transition duration-75 group-hover:bg-slate-100 dark:bg-slate-500 dark:group-hover:bg-slate-600">
|
||||
⏎
|
||||
</ShortcutIcon>
|
||||
<Body className="text-slate-500">to select</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowKeysUpDownIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="text-slate-500">to navigate</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<EscapeKeyIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="text-slate-500">to close</Body>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { ArrayIcon } from "./Icons/ArrayIcon";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
|
||||
export type SearchItemProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function SearchItem(className: SearchItemProps) {
|
||||
return (
|
||||
<li className="w-full rounded-sm mb-2 last:mb-10 group dark:bg-slate-900 transition hover:cursor-pointer hover:dark:bg-indigo-700">
|
||||
<div className="main-container flex items-center w-full p-2 pl-4 pr-3">
|
||||
<ArrayIcon className="h-6 w-6 text-slate-400 transition group-hover:text-white"></ArrayIcon>
|
||||
<div className="flex flex-col w-full ml-3">
|
||||
<div className="path flex items-center gap-1 mb-1 text-white">
|
||||
<Body className="text-xl">item</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">includes</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">tweets</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">…</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">0</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">size</Body>
|
||||
<ChevronRightIcon className="w-4 h-4"></ChevronRightIcon>
|
||||
<Body className="text-xl">date</Body>
|
||||
</div>
|
||||
<div className="key-value flex justify-between">
|
||||
<Mono className="text-slate-500 transition group-hover:text-white">
|
||||
<span className="text-indigo-500 transition group-hover:text-white group-hover:underline group-hover:underline-offset-1">
|
||||
2019
|
||||
</span>
|
||||
-12-31T19:26:16.000Z
|
||||
</Mono>
|
||||
<Mono className="text-slate-500 transition group-hover:text-white">
|
||||
Tue, Dec 31,
|
||||
<span className="text-indigo-500 transition group-hover:text-white group-hover:underline group-hover:underline-offset-1">
|
||||
2019
|
||||
</span>
|
||||
, 7:26:16 PM GMT
|
||||
</Mono>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,378 @@
|
||||
import { useJsonSearchApi, useJsonSearchState } from "~/hooks/useJsonSearch";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ExclamationIcon,
|
||||
SearchIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { EscapeKeyIcon } from "./Icons/EscapeKeyIcon";
|
||||
import { ArrowKeysUpDownIcon } from "./Icons/ArrowKeysUpDownIcon";
|
||||
import { LoadingIcon } from "./Icons/LoadingIcon";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { ShortcutIcon } from "./Icons/ShortcutIcon";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
import {
|
||||
useCombobox,
|
||||
UseComboboxState,
|
||||
UseComboboxStateChangeOptions,
|
||||
} from "downshift";
|
||||
import { JsonSearchEntry } from "~/utilities/search";
|
||||
import Fuse from "fuse.js";
|
||||
import classnames from "~/utilities/classnames";
|
||||
import { iconForValue } from "~/utilities/icons";
|
||||
import { useRef, useCallback } from "react";
|
||||
import { useVirtual } from "react-virtual";
|
||||
import { truncate } from "lodash-es";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export function SearchPalette() {
|
||||
export function SearchPalette({
|
||||
onSelect,
|
||||
onClose,
|
||||
}: {
|
||||
onSelect?: (entry: JsonSearchEntry) => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const searchState = useJsonSearchState();
|
||||
const searchApi = useJsonSearchApi();
|
||||
|
||||
useHotkeys(
|
||||
"esc",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
searchApi.reset();
|
||||
onClose?.();
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const listRef = useRef<HTMLElement>(null);
|
||||
|
||||
const rowVirtualizer = useVirtual({
|
||||
size: (searchState.results ?? []).length,
|
||||
parentRef: listRef,
|
||||
estimateSize: useCallback(() => 70, []),
|
||||
overscan: 6,
|
||||
});
|
||||
|
||||
function comboboxReducer(
|
||||
state: UseComboboxState<Fuse.FuseResult<JsonSearchEntry>>,
|
||||
actionAndChanges: UseComboboxStateChangeOptions<
|
||||
Fuse.FuseResult<JsonSearchEntry>
|
||||
>
|
||||
): Partial<UseComboboxState<Fuse.FuseResult<JsonSearchEntry>>> {
|
||||
const { changes, ...action } = actionAndChanges;
|
||||
|
||||
// Don't update the input field when selecting an item
|
||||
switch (action.type) {
|
||||
case useCombobox.stateChangeTypes.ItemClick:
|
||||
case useCombobox.stateChangeTypes.InputKeyDownEnter: {
|
||||
return {
|
||||
...changes,
|
||||
inputValue: state.inputValue,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
|
||||
const cb = useCombobox({
|
||||
items: searchState.results ?? [],
|
||||
stateReducer: comboboxReducer,
|
||||
circularNavigation: false,
|
||||
scrollIntoView: () => {},
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
if (selectedItem) {
|
||||
onSelect?.(selectedItem.item);
|
||||
searchApi.reset();
|
||||
}
|
||||
},
|
||||
onHighlightedIndexChange: ({ highlightedIndex }) =>
|
||||
highlightedIndex && rowVirtualizer.scrollToIndex(highlightedIndex),
|
||||
onInputValueChange: ({ inputValue }) =>
|
||||
inputValue ? searchApi.search(inputValue) : searchApi.reset(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>Search json</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchState.query ?? ""}
|
||||
onChange={(e) => searchApi.search(e.currentTarget.value)}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
{...cb.getComboboxProps()}
|
||||
className="search-container max-h-[60vh] px-4 pt-4 overflow-hidden"
|
||||
>
|
||||
<label
|
||||
{...cb.getLabelProps()}
|
||||
className="relative text-gray-400 focus-within:text-gray-600 block"
|
||||
>
|
||||
<SearchIcon className="absolute w-7 h-7 top-1/2 transform -translate-y-1/2 left-3 text-white pointer-events-none" />
|
||||
<input
|
||||
{...cb.getInputProps()}
|
||||
type="text"
|
||||
placeholder="Search the JSON…"
|
||||
className="search-field w-full pl-12 pr-4 py-4 rounded-sm text-white text-2xl caret-indigo-700 bg-slate-900 border-indigo-700 focus:outline-none focus:ring focus:ring-indigo-700"
|
||||
/>
|
||||
</label>
|
||||
<div className="search-content flex flex-col mt-4 mb-2">
|
||||
<div className="results flex">
|
||||
{searchState.status !== "idle" &&
|
||||
(!searchState.results || searchState.results.length === 0) && (
|
||||
<div className="results-loading flex">
|
||||
<LoadingIcon className="animate-spin h-5 w-5 mr-1"></LoadingIcon>
|
||||
<Body className="text-slate-400">Loading…</Body>
|
||||
</div>
|
||||
)}
|
||||
{searchState.results && searchState.results.length > 0 && (
|
||||
<div className="results-returned">
|
||||
<Body className="text-slate-400">
|
||||
{searchState.results.length === 1
|
||||
? "1 result"
|
||||
: `${searchState.results.length} results`}
|
||||
</Body>
|
||||
</div>
|
||||
)}
|
||||
{searchState.status === "idle" &&
|
||||
searchState.query &&
|
||||
searchState.query.length > 1 &&
|
||||
(!searchState.results || searchState.results.length === 0) && (
|
||||
<div className="results-none flex">
|
||||
<ExclamationIcon className="h-5 w-5 mr-1 text-white"></ExclamationIcon>
|
||||
<Body className="text-slate-400">
|
||||
No results for "{cb.inputValue}"
|
||||
</Body>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
{...cb.getMenuProps({ ref: listRef })}
|
||||
className="search-items w-full max-h-[inherit] pb-24 overflow-y-auto relative"
|
||||
>
|
||||
<li key="total-size" style={{ height: rowVirtualizer.totalSize }} />
|
||||
{rowVirtualizer.virtualItems.map((virtualRow) => {
|
||||
const result = (searchState.results ?? [])[virtualRow.index];
|
||||
|
||||
return (
|
||||
<SearchItem
|
||||
key={result.item.path}
|
||||
itemProps={cb.getItemProps({
|
||||
item: result,
|
||||
index: virtualRow.index,
|
||||
style: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
},
|
||||
})}
|
||||
result={result}
|
||||
isHighlighted={virtualRow.index === cb.highlightedIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<ul>
|
||||
{searchState.results?.map((result) => (
|
||||
<li key={result.item.path}>
|
||||
[{result.item.path}] {result.item.formattedValue}
|
||||
{" - "}
|
||||
{result.item.rawValue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="footer flex items-center w-full gap-4 px-3 py-2 border-t-[1px] border-slate-700 rounded-br-lg rounded-bl-lg bg-slate-900">
|
||||
<div className="flex items-center gap-1">
|
||||
<ShortcutIcon className="w-4 h-4 text-sm text-slate-900 bg-slate-300 transition duration-75 group-hover:bg-slate-100 dark:bg-slate-500 dark:group-hover:bg-slate-600">
|
||||
⏎
|
||||
</ShortcutIcon>
|
||||
<Body className="text-slate-500">to select</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowKeysUpDownIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="text-slate-500">to navigate</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<EscapeKeyIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="text-slate-500">to close</Body>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type SearchItemProps = {
|
||||
itemProps: React.HTMLAttributes<HTMLLIElement>;
|
||||
result: Fuse.FuseResult<JsonSearchEntry>;
|
||||
isHighlighted: boolean;
|
||||
};
|
||||
|
||||
export function SearchItem({
|
||||
itemProps,
|
||||
result,
|
||||
isHighlighted,
|
||||
}: SearchItemProps) {
|
||||
const ItemIcon = iconForValue(result.item.rawValue);
|
||||
|
||||
const heroPath = new JSONHeroPath(result.item.path);
|
||||
|
||||
const components = heroPath.components.slice(1);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...itemProps}
|
||||
className={classnames(
|
||||
"w-full rounded-sm mb-2 last:mb-10 group transition hover:cursor-pointer",
|
||||
isHighlighted ? "dark:bg-indigo-700" : "dark:bg-slate-900"
|
||||
)}
|
||||
>
|
||||
<div className="main-container flex items-center w-full p-2 pl-4 pr-3">
|
||||
<ItemIcon className="h-6 w-6 text-slate-400 transition group-hover:text-white"></ItemIcon>
|
||||
<div className="flex flex-col w-full ml-3">
|
||||
<div className="path flex items-center gap-1 mb-1 text-white">
|
||||
{components.map((c, index) => {
|
||||
return [
|
||||
<Body key={c.toString() + index + "body"} className="text-lg">
|
||||
{c.toString()}
|
||||
</Body>,
|
||||
].concat(
|
||||
index + 1 === components.length
|
||||
? []
|
||||
: [
|
||||
<ChevronRightIcon
|
||||
key={c.toString() + index + "arrow"}
|
||||
className="w-4 h-4"
|
||||
/>,
|
||||
]
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="key-value flex justify-between">
|
||||
{result.item.rawValue && (
|
||||
<SearchResultValue
|
||||
isHighlighted={isHighlighted}
|
||||
keyName="rawValue"
|
||||
stringValue={result.item.rawValue}
|
||||
searchResult={result}
|
||||
/>
|
||||
)}
|
||||
{result.item.formattedValue &&
|
||||
result.item.formattedValue !== result.item.rawValue && (
|
||||
<SearchResultValue
|
||||
isHighlighted={isHighlighted}
|
||||
keyName="formattedValue"
|
||||
stringValue={result.item.formattedValue}
|
||||
searchResult={result}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResultValue({
|
||||
isHighlighted,
|
||||
keyName,
|
||||
stringValue,
|
||||
searchResult,
|
||||
}: {
|
||||
isHighlighted: boolean;
|
||||
keyName: string;
|
||||
stringValue: string;
|
||||
searchResult: Fuse.FuseResult<JsonSearchEntry>;
|
||||
}) {
|
||||
const match = (searchResult.matches ?? []).find(
|
||||
(match) => match.key === keyName
|
||||
);
|
||||
|
||||
const output = createOutputForMatch(stringValue, isHighlighted, match);
|
||||
|
||||
return (
|
||||
<Mono
|
||||
className={classnames(
|
||||
"text-slate-500 transition",
|
||||
isHighlighted ? "dark:text-white" : ""
|
||||
)}
|
||||
>
|
||||
{output}
|
||||
</Mono>
|
||||
);
|
||||
}
|
||||
|
||||
function createOutputForMatch(
|
||||
stringValue: string,
|
||||
isHighlighted: boolean,
|
||||
match?: Fuse.FuseResultMatch
|
||||
): JSX.Element {
|
||||
if (!match) {
|
||||
return <>{truncate(stringValue, { length: 56 })}</>;
|
||||
}
|
||||
|
||||
if (stringValue.length <= 56) {
|
||||
const stringSlices = getAllStringSlices(stringValue, match.indices);
|
||||
|
||||
return (
|
||||
<>
|
||||
{stringSlices.map((s, index) => {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className={
|
||||
s.isMatch
|
||||
? classnames(
|
||||
"text-indigo-500 transition",
|
||||
isHighlighted
|
||||
? "text-white underline underline-offset-1"
|
||||
: ""
|
||||
)
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{s.slice}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{truncate(stringValue, { length: 56 })}</>;
|
||||
}
|
||||
|
||||
type StringSlice = {
|
||||
start: number;
|
||||
end: number;
|
||||
isMatch: boolean;
|
||||
slice: string;
|
||||
};
|
||||
|
||||
function getAllStringSlices(
|
||||
stringValue: string,
|
||||
matchingIndices: ReadonlyArray<[number, number]>
|
||||
): Array<StringSlice> {
|
||||
const slices: StringSlice[] = [];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const addSlice = (
|
||||
start: number,
|
||||
end: number,
|
||||
isMatch: boolean,
|
||||
slice: string
|
||||
) => {
|
||||
slices.push({ start, end, isMatch, slice });
|
||||
};
|
||||
|
||||
for (const [start, end] of matchingIndices) {
|
||||
addSlice(
|
||||
currentIndex,
|
||||
start,
|
||||
false,
|
||||
stringValue.slice(currentIndex, start)
|
||||
);
|
||||
addSlice(start, end + 1, true, stringValue.slice(start, end + 1));
|
||||
currentIndex = end + 1;
|
||||
}
|
||||
|
||||
addSlice(
|
||||
currentIndex,
|
||||
stringValue.length,
|
||||
false,
|
||||
stringValue.slice(currentIndex)
|
||||
);
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
+5
-2
@@ -29,7 +29,6 @@ self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
|
||||
|
||||
console.group(`SearchWorker: ${type}`);
|
||||
console.log(payload);
|
||||
console.groupEnd();
|
||||
|
||||
switch (type) {
|
||||
case "initialize-index": {
|
||||
@@ -54,7 +53,11 @@ self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
|
||||
|
||||
const results = self.fuse.search(query);
|
||||
|
||||
self.postMessage({ type: "search-results", payload: { results } });
|
||||
console.log("results", results);
|
||||
|
||||
self.postMessage({ type: "search-results", payload: { results, query } });
|
||||
}
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
+114
-26
@@ -28,7 +28,7 @@ export type IndexInitializedEvent = {
|
||||
|
||||
export type SearchResultsEvent = {
|
||||
type: "search-results";
|
||||
payload: { results: Fuse.FuseResult<JsonSearchEntry>[] };
|
||||
payload: { results: Fuse.FuseResult<JsonSearchEntry>[]; query: string };
|
||||
};
|
||||
|
||||
export type SearchReceiveWorkerEvent =
|
||||
@@ -37,6 +37,7 @@ export type SearchReceiveWorkerEvent =
|
||||
|
||||
export type JsonSearchApi = {
|
||||
search: (query: string) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const JsonSearchStateContext = createContext<JsonSearchState>(
|
||||
@@ -56,36 +57,119 @@ type SearchAction = {
|
||||
payload: { query: string };
|
||||
};
|
||||
|
||||
type JsonSearchAction = SearchReceiveWorkerEvent | SearchAction;
|
||||
type ResetAction = {
|
||||
type: "reset";
|
||||
};
|
||||
|
||||
type JsonSearchAction = SearchReceiveWorkerEvent | SearchAction | ResetAction;
|
||||
|
||||
function reducer(
|
||||
state: JsonSearchState,
|
||||
action: JsonSearchAction
|
||||
): JsonSearchState {
|
||||
switch (action.type) {
|
||||
case "index-initialized":
|
||||
return {
|
||||
...state,
|
||||
status: "idle",
|
||||
results: undefined,
|
||||
};
|
||||
case "search-results":
|
||||
return {
|
||||
...state,
|
||||
status: "idle",
|
||||
results: action.payload.results,
|
||||
};
|
||||
case "search":
|
||||
return {
|
||||
...state,
|
||||
status: "searching",
|
||||
query: action.payload.query,
|
||||
};
|
||||
default:
|
||||
switch (state.status) {
|
||||
case "initializing": {
|
||||
if (action.type === "index-initialized") {
|
||||
return {
|
||||
...state,
|
||||
status: "idle",
|
||||
results: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case "idle": {
|
||||
if (action.type === "reset") {
|
||||
return {
|
||||
...state,
|
||||
query: undefined,
|
||||
results: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === "search") {
|
||||
return {
|
||||
...state,
|
||||
status: "searching",
|
||||
query: action.payload.query,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case "searching": {
|
||||
if (action.type === "reset") {
|
||||
return {
|
||||
...state,
|
||||
status: "idle",
|
||||
query: undefined,
|
||||
results: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
action.type === "search-results" &&
|
||||
state.query === action.payload.query
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
status: "idle",
|
||||
results: action.payload.results,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastAction: any | undefined;
|
||||
|
||||
function wrapReducer<S, A extends { type: string }>(
|
||||
name: string,
|
||||
reducer: React.Reducer<S, A>
|
||||
): React.Reducer<S, A> {
|
||||
return (state, action) => {
|
||||
const next = reducer(state, action);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (!lastAction) {
|
||||
console.groupCollapsed(
|
||||
`%cAction: %c${
|
||||
name + " " + action.type
|
||||
} %cat ${getCurrentTimeFormatted()}`,
|
||||
"color: lightgreen; font-weight: bold;",
|
||||
"color: white; font-weight: bold;",
|
||||
"color: lightblue; font-weight: lighter;"
|
||||
);
|
||||
console.log(
|
||||
"%cPrevious State:",
|
||||
"color: #9E9E9E; font-weight: 700;",
|
||||
state
|
||||
);
|
||||
console.log("%cAction:", "color: #00A7F7; font-weight: 700;", action);
|
||||
console.log("%cNext State:", "color: #47B04B; font-weight: 700;", next);
|
||||
console.groupEnd();
|
||||
lastAction = action;
|
||||
} else {
|
||||
lastAction = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
}
|
||||
|
||||
const getCurrentTimeFormatted = () => {
|
||||
const currentTime = new Date();
|
||||
const hours = currentTime.getHours();
|
||||
const minutes = currentTime.getMinutes();
|
||||
const seconds = currentTime.getSeconds();
|
||||
const milliseconds = currentTime.getMilliseconds();
|
||||
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
};
|
||||
|
||||
export function JsonSearchProvider({
|
||||
children,
|
||||
}: {
|
||||
@@ -95,7 +179,7 @@ export function JsonSearchProvider({
|
||||
|
||||
const [state, dispatch] = useReducer<
|
||||
React.Reducer<JsonSearchState, JsonSearchAction>
|
||||
>(reducer, { status: "initializing" });
|
||||
>(wrapReducer("jsonSearch", reducer), { status: "initializing" });
|
||||
|
||||
const search = useCallback(
|
||||
(query: string) => {
|
||||
@@ -104,6 +188,10 @@ export function JsonSearchProvider({
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
dispatch({ type: "reset" });
|
||||
}, [dispatch]);
|
||||
|
||||
const handleWorkerMessage = useCallback(
|
||||
(e: MessageEvent<SearchReceiveWorkerEvent>) => dispatch(e.data),
|
||||
[dispatch]
|
||||
@@ -132,10 +220,10 @@ export function JsonSearchProvider({
|
||||
fuseOptions: {
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 1,
|
||||
minMatchCharLength: 2,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.6,
|
||||
distance: 200,
|
||||
distance: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -154,7 +242,7 @@ export function JsonSearchProvider({
|
||||
|
||||
return (
|
||||
<JsonSearchStateContext.Provider value={state}>
|
||||
<JsonSearchApiContext.Provider value={{ search }}>
|
||||
<JsonSearchApiContext.Provider value={{ search, reset }}>
|
||||
{children}
|
||||
</JsonSearchApiContext.Provider>
|
||||
</JsonSearchStateContext.Provider>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { inferType, JSONValueType } from "@jsonhero/json-infer-types";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { IconComponent } from "~/useColumnView";
|
||||
import { formatValue } from "~/utilities/formatter";
|
||||
import { iconForType } from "~/utilities/jsonColumnView";
|
||||
import { iconForType } from "~/utilities/icons";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
|
||||
@@ -256,7 +256,7 @@ export type ResetSelectionNodeAction = {
|
||||
|
||||
export type GoAction = {
|
||||
type: "GO";
|
||||
direction: number;
|
||||
direction: -1 | 1;
|
||||
};
|
||||
|
||||
export type ColumnViewAction =
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
CubeIcon,
|
||||
CollectionIcon,
|
||||
EyeOffIcon,
|
||||
CheckCircleIcon,
|
||||
AnnotationIcon,
|
||||
CalendarIcon,
|
||||
AtSymbolIcon,
|
||||
GlobeAltIcon,
|
||||
PhotographIcon,
|
||||
CodeIcon,
|
||||
PhoneIcon,
|
||||
DocumentIcon,
|
||||
ColorSwatchIcon,
|
||||
CreditCardIcon,
|
||||
CurrencyDollarIcon,
|
||||
ClockIcon,
|
||||
GlobeIcon,
|
||||
EmojiHappyIcon,
|
||||
ChatAlt2Icon,
|
||||
ArchiveIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
DocumentTextIcon,
|
||||
HashtagIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { inferType, JSONValueType } from "@jsonhero/json-infer-types";
|
||||
import { StringIcon } from "~/components/Icons/StringIcon";
|
||||
import { IconComponent } from "~/useColumnView";
|
||||
|
||||
export function iconForValue(value: unknown): IconComponent {
|
||||
return iconForType(inferType(value));
|
||||
}
|
||||
|
||||
export function iconForType(type: JSONValueType): IconComponent {
|
||||
switch (type.name) {
|
||||
case "object": {
|
||||
return CubeIcon;
|
||||
}
|
||||
case "array": {
|
||||
return CollectionIcon;
|
||||
}
|
||||
case "null": {
|
||||
return EyeOffIcon;
|
||||
}
|
||||
case "bool": {
|
||||
return CheckCircleIcon;
|
||||
}
|
||||
case "int":
|
||||
case "float": {
|
||||
return HashtagIcon;
|
||||
}
|
||||
case "string": {
|
||||
if (type.format == null) {
|
||||
return StringIcon;
|
||||
}
|
||||
|
||||
switch (type.format.name) {
|
||||
case "timestamp": {
|
||||
return CalendarIcon;
|
||||
}
|
||||
case "datetime": {
|
||||
switch (type.format.parts) {
|
||||
case "time":
|
||||
return ClockIcon;
|
||||
}
|
||||
return ClockIcon;
|
||||
}
|
||||
case "email": {
|
||||
return AtSymbolIcon;
|
||||
}
|
||||
case "hostname":
|
||||
case "tld":
|
||||
case "ip":
|
||||
return GlobeAltIcon;
|
||||
case "uri": {
|
||||
switch (type.format.contentType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
case "image/webm":
|
||||
return PhotographIcon;
|
||||
case "application/json":
|
||||
return CodeIcon;
|
||||
default:
|
||||
return GlobeAltIcon;
|
||||
}
|
||||
}
|
||||
case "phoneNumber": {
|
||||
return PhoneIcon;
|
||||
}
|
||||
case "currency": {
|
||||
return CurrencyDollarIcon;
|
||||
}
|
||||
case "country": {
|
||||
return GlobeIcon;
|
||||
}
|
||||
case "emoji": {
|
||||
return EmojiHappyIcon;
|
||||
}
|
||||
case "color": {
|
||||
return ColorSwatchIcon;
|
||||
}
|
||||
case "language": {
|
||||
return ChatAlt2Icon;
|
||||
}
|
||||
case "filesize": {
|
||||
return ArchiveIcon;
|
||||
}
|
||||
case "uuid": {
|
||||
return IdentificationIcon;
|
||||
}
|
||||
case "json":
|
||||
case "jsonPointer": {
|
||||
return CodeIcon;
|
||||
}
|
||||
case "jwt": {
|
||||
return KeyIcon;
|
||||
}
|
||||
case "semver": {
|
||||
return DocumentTextIcon;
|
||||
}
|
||||
case "creditcard": {
|
||||
switch (type.format.variant) {
|
||||
case "visa": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "mastercard": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "amex": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "discover": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "dinersclub": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
default: {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return AnnotationIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return DocumentIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,8 @@
|
||||
import {
|
||||
CubeIcon,
|
||||
CollectionIcon,
|
||||
EyeOffIcon,
|
||||
CheckCircleIcon,
|
||||
AnnotationIcon,
|
||||
CalendarIcon,
|
||||
AtSymbolIcon,
|
||||
GlobeAltIcon,
|
||||
PhotographIcon,
|
||||
CodeIcon,
|
||||
PhoneIcon,
|
||||
DocumentIcon,
|
||||
ColorSwatchIcon,
|
||||
CreditCardIcon,
|
||||
CurrencyDollarIcon,
|
||||
ClockIcon,
|
||||
GlobeIcon,
|
||||
EmojiHappyIcon,
|
||||
ChatAlt2Icon,
|
||||
ArchiveIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
DocumentTextIcon,
|
||||
HashtagIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { inferType, JSONValueType } from "@jsonhero/json-infer-types";
|
||||
import { JSONHeroPath, PathComponent } from "@jsonhero/path";
|
||||
import { StringIcon } from "~/components/Icons/StringIcon";
|
||||
import { ColumnViewNode, IconComponent } from "~/useColumnView";
|
||||
import { ColumnViewNode } from "~/useColumnView";
|
||||
import { formatValue } from "./formatter";
|
||||
import { iconForType } from "./icons";
|
||||
|
||||
export function generateColumnViewNode(json: unknown): ColumnViewNode {
|
||||
const info = inferType(json);
|
||||
@@ -117,127 +91,6 @@ export function generateNodesToPath(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function iconForType(type: JSONValueType): IconComponent {
|
||||
switch (type.name) {
|
||||
case "object": {
|
||||
return CubeIcon;
|
||||
}
|
||||
case "array": {
|
||||
return CollectionIcon;
|
||||
}
|
||||
case "null": {
|
||||
return EyeOffIcon;
|
||||
}
|
||||
case "bool": {
|
||||
return CheckCircleIcon;
|
||||
}
|
||||
case "int":
|
||||
case "float": {
|
||||
return HashtagIcon;
|
||||
}
|
||||
case "string": {
|
||||
if (type.format == null) {
|
||||
return StringIcon;
|
||||
}
|
||||
|
||||
switch (type.format.name) {
|
||||
case "timestamp": {
|
||||
return CalendarIcon;
|
||||
}
|
||||
case "datetime": {
|
||||
switch (type.format.parts) {
|
||||
case "time":
|
||||
return ClockIcon;
|
||||
}
|
||||
return ClockIcon;
|
||||
}
|
||||
case "email": {
|
||||
return AtSymbolIcon;
|
||||
}
|
||||
case "hostname":
|
||||
case "tld":
|
||||
case "ip":
|
||||
return GlobeAltIcon;
|
||||
case "uri": {
|
||||
switch (type.format.contentType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
case "image/webm":
|
||||
return PhotographIcon;
|
||||
case "application/json":
|
||||
return CodeIcon;
|
||||
default:
|
||||
return GlobeAltIcon;
|
||||
}
|
||||
}
|
||||
case "phoneNumber": {
|
||||
return PhoneIcon;
|
||||
}
|
||||
case "currency": {
|
||||
return CurrencyDollarIcon;
|
||||
}
|
||||
case "country": {
|
||||
return GlobeIcon;
|
||||
}
|
||||
case "emoji": {
|
||||
return EmojiHappyIcon;
|
||||
}
|
||||
case "color": {
|
||||
return ColorSwatchIcon;
|
||||
}
|
||||
case "language": {
|
||||
return ChatAlt2Icon;
|
||||
}
|
||||
case "filesize": {
|
||||
return ArchiveIcon;
|
||||
}
|
||||
case "uuid": {
|
||||
return IdentificationIcon;
|
||||
}
|
||||
case "json":
|
||||
case "jsonPointer": {
|
||||
return CodeIcon;
|
||||
}
|
||||
case "jwt": {
|
||||
return KeyIcon;
|
||||
}
|
||||
case "semver": {
|
||||
return DocumentTextIcon;
|
||||
}
|
||||
case "creditcard": {
|
||||
switch (type.format.variant) {
|
||||
case "visa": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "mastercard": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "amex": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "discover": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
case "dinersclub": {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
default: {
|
||||
return CreditCardIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return AnnotationIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return DocumentIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function firstChildToDescendant(
|
||||
ancestor: JSONHeroPath,
|
||||
descendant: JSONHeroPath
|
||||
|
||||
@@ -83,3 +83,49 @@ export function getRawValue(type: JSONValueType): string | undefined {
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
type StringSlice = {
|
||||
start: number;
|
||||
end: number;
|
||||
isMatch: boolean;
|
||||
slice: string;
|
||||
};
|
||||
|
||||
export function getStringSlices(
|
||||
stringValue: string,
|
||||
matchingIndices: ReadonlyArray<[number, number]>,
|
||||
maxLength: number
|
||||
): Array<StringSlice> {
|
||||
const slices: StringSlice[] = [];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const addSlice = (
|
||||
start: number,
|
||||
end: number,
|
||||
isMatch: boolean,
|
||||
slice: string
|
||||
) => {
|
||||
slices.push({ start, end, isMatch, slice });
|
||||
};
|
||||
|
||||
for (const [start, end] of matchingIndices) {
|
||||
addSlice(
|
||||
currentIndex,
|
||||
start,
|
||||
false,
|
||||
stringValue.slice(currentIndex, start)
|
||||
);
|
||||
addSlice(start, end + 1, true, stringValue.slice(start, end + 1));
|
||||
currentIndex = end + 1;
|
||||
}
|
||||
|
||||
addSlice(
|
||||
currentIndex,
|
||||
stringValue.length,
|
||||
false,
|
||||
stringValue.slice(currentIndex)
|
||||
);
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
Generated
+66
-6
@@ -25,6 +25,7 @@
|
||||
"@uiw/react-codemirror": "^4.3.3",
|
||||
"clsx": "^1.1.1",
|
||||
"color": "^4.2.1",
|
||||
"downshift": "^6.1.7",
|
||||
"fathom-client": "^3.4.1",
|
||||
"framer-motion": "^6.2.4",
|
||||
"fuse.js": "^6.5.3",
|
||||
@@ -1703,9 +1704,9 @@
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@jsonhero/json-infer-types": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.8.tgz",
|
||||
"integrity": "sha512-a0Ish/uHFI1AJcwpiCnK73uA2/XceffeysiqMJaX3V7AwUs5Xox1s/zjiUix959JLWbHnxsQDPPL1M8j560lyw==",
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.9.tgz",
|
||||
"integrity": "sha512-QuIOEy7M57bB4fhwadtDUQv5HjrUFD9SXjvQfWUMSQ6zOL8l9S9SV/wRQCn0y0dal+xZPBoMoDqTMJyEBIHnQQ==",
|
||||
"dependencies": {
|
||||
"ip-address": "^8.1.0",
|
||||
"json5": "^2.2.0",
|
||||
@@ -4167,6 +4168,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz",
|
||||
"integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -4776,6 +4782,31 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.7.tgz",
|
||||
"integrity": "sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"compute-scroll-into-view": "^1.0.17",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/downshift/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -14842,9 +14873,9 @@
|
||||
}
|
||||
},
|
||||
"@jsonhero/json-infer-types": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.8.tgz",
|
||||
"integrity": "sha512-a0Ish/uHFI1AJcwpiCnK73uA2/XceffeysiqMJaX3V7AwUs5Xox1s/zjiUix959JLWbHnxsQDPPL1M8j560lyw==",
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.9.tgz",
|
||||
"integrity": "sha512-QuIOEy7M57bB4fhwadtDUQv5HjrUFD9SXjvQfWUMSQ6zOL8l9S9SV/wRQCn0y0dal+xZPBoMoDqTMJyEBIHnQQ==",
|
||||
"requires": {
|
||||
"ip-address": "^8.1.0",
|
||||
"json5": "^2.2.0",
|
||||
@@ -16822,6 +16853,11 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"compute-scroll-into-view": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz",
|
||||
"integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -17279,6 +17315,30 @@
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"dev": true
|
||||
},
|
||||
"downshift": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.7.tgz",
|
||||
"integrity": "sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"compute-scroll-into-view": "^1.0.17",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@
|
||||
"@uiw/react-codemirror": "^4.3.3",
|
||||
"clsx": "^1.1.1",
|
||||
"color": "^4.2.1",
|
||||
"downshift": "^6.1.7",
|
||||
"fathom-client": "^3.4.1",
|
||||
"framer-motion": "^6.2.4",
|
||||
"fuse.js": "^6.5.3",
|
||||
@@ -103,4 +104,4 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"main": "dist/worker.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Fuse from "fuse.js";
|
||||
import {
|
||||
createSearchEntries,
|
||||
createSearchIndex,
|
||||
getStringSlices,
|
||||
} from "../app/utilities/search";
|
||||
|
||||
const json = {
|
||||
@@ -57,6 +58,39 @@ const json = {
|
||||
],
|
||||
};
|
||||
|
||||
describe("getStringSlices", () => {
|
||||
it("returns a slice for each part of the string based on the matches", () => {
|
||||
const slices = getStringSlices(
|
||||
"This is a really great (short) string",
|
||||
[[9, 16]],
|
||||
60
|
||||
);
|
||||
|
||||
expect(slices).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"end": 9,
|
||||
"isMatch": false,
|
||||
"slice": "This is a",
|
||||
"start": 0,
|
||||
},
|
||||
Object {
|
||||
"end": 17,
|
||||
"isMatch": true,
|
||||
"slice": " really ",
|
||||
"start": 9,
|
||||
},
|
||||
Object {
|
||||
"end": 37,
|
||||
"isMatch": false,
|
||||
"slice": "great (short) string",
|
||||
"start": 17,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSearchIndex", () => {
|
||||
it("creates a search index that can search keys, raw values, and formatted values", () => {
|
||||
const [index, entries] = createSearchIndex(json);
|
||||
|
||||
Reference in New Issue
Block a user