Skip to content

Commit

Permalink
[Nu-1780] Expand search options
Browse files Browse the repository at this point in the history
  • Loading branch information
Piotr Rudnicki committed Aug 30, 2024
1 parent 1b900ea commit 6e030c5
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function SearchPanel(props: ToolbarPanelProps): ReactElement {
>
<SearchIcon isEmpty={isEmpty(filter)} />
</SearchInputWithIcon>
<SearchResults filterValues={[filter.toLowerCase()].filter(Boolean)} />
<SearchResults filterRawText={filter} />
</ToolbarWrapper>
);
}
36 changes: 32 additions & 4 deletions designer/client/src/components/toolbars/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,38 @@ import { getScenario, getSelectionState } from "../../../reducers/selectors/grap
import { MenuItem, MenuList } from "@mui/material";
import { FoundNode } from "./FoundNode";
import React, { useCallback, useEffect, useState } from "react";
import { useFilteredNodes } from "./utils";
import { resolveSearchOption, useFilteredNodes } from "./utils";
import { useGraph } from "../../graph/GraphContext";
import { nodeFound, nodeFoundHover } from "../../graph/graphStyledWrapper";
import { resetSelection } from "../../../actions/nk";
import { useWindows } from "../../../windowManager";

export function SearchResults({ filterValues = [] }: { filter?: string; filterValues?: string[] }) {
const nodes = useFilteredNodes(filterValues);
export enum SearchType {
SIMPLE,
ADVANCED,
}

export type SimpleSearch = {
searchType: SearchType.SIMPLE;
query: string;
};

export type AdvancedSearch = {
searchType: SearchType.ADVANCED;
id?: string[];
description?: string[];
type?: string[];
paramName?: string[];
paramValue?: string[];
outputValue?: string[];
edgeExpression?: string[];
};

export type SearchOption = SimpleSearch | AdvancedSearch;

export function SearchResults({ filterRawText }: { filterRawText?: string }) {
const searchOption: SearchOption = resolveSearchOption(filterRawText);
const nodes = useFilteredNodes(searchOption);

const [hasFocus, setHasFocus] = useState(false);
const [hoveredNodes, setHoveredNodes] = useState<string[]>([]);
Expand Down Expand Up @@ -83,7 +107,11 @@ export function SearchResults({ filterValues = [] }: { filter?: string; filterVa
disableGutters
divider
>
<FoundNode node={node} highlights={filterValues} fields={groups} />
<FoundNode
node={node}
highlights={searchOption.searchType === SearchType.SIMPLE ? [searchOption.query] : searchOption.id}
fields={groups}
/>
</MenuItem>
))}
</MenuList>
Expand Down
117 changes: 106 additions & 11 deletions designer/client/src/components/toolbars/search/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import NodeUtils from "../../graph/NodeUtils";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ensureArray } from "../../../common/arrayUtils";
import { AdvancedSearch, SearchOption, SearchType, SimpleSearch } from "./SearchResults";

type SelectorResult = { expression: string } | string;
type Selector = (node: NodeType) => SelectorResult | SelectorResult[];
Expand Down Expand Up @@ -52,7 +53,7 @@ const fieldsSelectors: FilterSelector = [

function matchFilters(value: SelectorResult, filterValues: string[]): boolean {
const resolved = typeof value === "string" ? value : value?.expression;
return filterValues.length && filterValues.every((filter) => resolved?.toLowerCase().includes(filter.toLowerCase()));
return filterValues.length && filterValues.some((filter) => filter != "" && resolved?.toLowerCase().includes(filter.toLowerCase()));
}

export const findFields = (filterValues: string[], node: NodeType) => {
Expand All @@ -65,11 +66,31 @@ export const findFields = (filterValues: string[], node: NodeType) => {
);
};

export function useFilteredNodes(filterValues: string[]): {
const findFieldsUsingSelectorWithName = (selectorName: string, filterValues: string[], node: NodeType) => {
return uniq(
fieldsSelectors
.filter((selector) => selector.name == selectorName)
.flatMap(({ name, selector }) =>
ensureArray(selector(node))
.filter((v) => matchFilters(v, filterValues))
.map(() => name),
),
);
};

function arraysEqual(arr1: string[], arr2: string[]): boolean {
if (arr1.length !== arr2.length) {
return false;
}
return arr1.slice().sort().join() === arr2.slice().sort().join();
}

export function useFilteredNodes(searchOption: SearchOption): {
groups: string[];
node: NodeType;
edges: Edge[];
}[] {
console.log(searchOption);
const { t } = useTranslation();
const { scenarioGraph } = useSelector(getScenario);
const allNodes = NodeUtils.nodesFromScenarioGraph(scenarioGraph);
Expand All @@ -92,18 +113,92 @@ export function useFilteredNodes(filterValues: string[]): {
() =>
allNodes
.map((node) => {
const edges = allEdges
.filter((e) => e.from === node.id)
.filter((e) => matchFilters(e.edgeType?.condition, filterValues));
let edges: Edge[] = [];
let groups: string[] = [];

switch (searchOption.searchType) {
case SearchType.SIMPLE: {
edges = allEdges
.filter((e) => e.from === node.id)
.filter((e) => matchFilters(e.edgeType?.condition, [searchOption.query]));

groups = findFields([searchOption.query], node)
.concat(edges.length ? "edgeExpression" : null)
.map((name) => displayNames[name])
.filter(Boolean);

console.log(groups);

return { node, edges, groups };
}
case SearchType.ADVANCED: {
edges =
"edgeExpression" in searchOption
? allEdges
.filter((e) => e.from === node.id)
.filter((e) => matchFilters(e.edgeType?.condition, searchOption.edgeExpression))
: [];

const keyNamesRelevantForFiltering = Object.keys(searchOption).filter((key) => key !== "searchType");

const groups = findFields(filterValues, node)
.concat(edges.length ? "edgeExpression" : null)
.map((name) => displayNames[name])
.filter(Boolean);
const displayKeyNamesRelevantForFiltering: string[] = keyNamesRelevantForFiltering.map(
(name) => displayNames[name],
);

return { node, edges, groups };
groups = keyNamesRelevantForFiltering
.map((key) => findFieldsUsingSelectorWithName(key, searchOption[key], node))
.flat()
.concat(edges.length ? "edgeExpression" : null)
.map((name) => displayNames[name])
.filter(Boolean);

const allChecksPassed = arraysEqual(groups, displayKeyNamesRelevantForFiltering);

if (!allChecksPassed) {
edges = [];
groups = [];
}

return { node, edges, groups };
}
}
})
.filter(({ groups }) => groups.length),
[displayNames, allEdges, filterValues, allNodes],
[displayNames, allEdges, searchOption, allNodes],
);
}

export function resolveSearchOption(filterRawText: string): SearchOption {
const advancedFilterKeyNames: string[] = [
"id:(",
"description:(",
"type:(",
"paramName:(",
"paramValue:(",
"outputValue:(",
"edgeExpression:(",
]; //heuristic, but sufficient
const isAdvancedSearchOption = advancedFilterKeyNames.some((advancedKeyName) => filterRawText.includes(advancedKeyName));

if (isAdvancedSearchOption) {
return parseRawTextToAdvancedSearch(filterRawText);
} else {
return { searchType: SearchType.SIMPLE, query: filterRawText } as SimpleSearch;
}
}

function parseRawTextToAdvancedSearch(text: string): AdvancedSearch {
const result: AdvancedSearch = { searchType: SearchType.ADVANCED };
const regex = /(\w+):\(([^)]*)\)/g;
let match: RegExpExecArray | null;

while ((match = regex.exec(text)) !== null) {
const key = match[1] as keyof Exclude<keyof AdvancedSearch, "searchType">;
console.log(key);
const values = match[2].split(",").map((value) => value.trim().replace(/"/g, ""));

result[key] = values.length > 0 ? values : undefined;
}

return result;
}

0 comments on commit 6e030c5

Please sign in to comment.