diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process view should have node search toolbar #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process view should have node search toolbar #1.png
index ced66534bc5..3316f3cb1e3 100644
Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process view should have node search toolbar #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process view should have node search toolbar #1.png differ
diff --git a/designer/client/cypress/e2e/search.cy.ts b/designer/client/cypress/e2e/search.cy.ts
index 2ecc7f16e17..b454ead3942 100644
--- a/designer/client/cypress/e2e/search.cy.ts
+++ b/designer/client/cypress/e2e/search.cy.ts
@@ -46,7 +46,7 @@ describe("Search Panel View", () => {
cy.get("[data-testid=search-panel]").find("input[name='type']").should("have.value", "sink");
cy.get("[data-testid=search-panel]").find("input[name='type']").click();
- cy.realType(",processor");
+ cy.realType(",dynamicService");
cy.get("[data-testid=search-panel]").find("button[type='submit']").click();
@@ -61,13 +61,13 @@ describe("Search Panel View", () => {
cy.get("[data-testid=search-panel]").find("svg[id='advanced-search-icon']").click();
cy.get("[data-testid=search-panel]").find("input[name='type']").click();
- cy.realType("sink,processor");
+ cy.realType("sink,dynamicSe");
cy.get("[data-testid=search-panel]").find("button[type='submit']").click();
cy.get("[data-testid=search-panel]")
.find("input[data-selector='NODES_IN_SCENARIO']")
- .should("have.value", "type:(sink,processor) se");
+ .should("have.value", "type:(sink,dynamicSe) se");
cy.get("[data-testid=search-panel]").contains("dynamicService");
cy.get("[data-testid=search-panel]").contains("sendSms");
@@ -76,7 +76,7 @@ describe("Search Panel View", () => {
it("should filter nodes when setting up multiple selectors using form", () => {
cy.get("[data-testid=search-panel]").find("svg[id='advanced-search-icon']").click();
- cy.get("[data-testid=search-panel]").find("input[name='id']").click();
+ cy.get("[data-testid=search-panel]").find("input[name='name']").click();
cy.realType("bounded,dynamic,send,enricher");
cy.get("[data-testid=search-panel]").find("input[name='type']").click();
@@ -86,7 +86,7 @@ describe("Search Panel View", () => {
cy.get("[data-testid=search-panel]")
.find("input[data-selector='NODES_IN_SCENARIO']")
- .should("have.value", "id:(bounded,dynamic,send,enricher) type:(sink,enricher) ");
+ .should("have.value", "name:(bounded,dynamic,send,enricher) type:(sink,enricher)");
cy.get("[data-testid=search-panel]").contains("enricher");
cy.get("[data-testid=search-panel]").contains("sendSms");
diff --git a/designer/client/src/components/sidePanels/SearchLabeledAutocomplete.tsx b/designer/client/src/components/sidePanels/SearchLabeledAutocomplete.tsx
new file mode 100644
index 00000000000..4bd32b9e85f
--- /dev/null
+++ b/designer/client/src/components/sidePanels/SearchLabeledAutocomplete.tsx
@@ -0,0 +1,30 @@
+import { Autocomplete, FormControl } from "@mui/material";
+import { nodeInput } from "../graph/node-modal/NodeDetailsContent/NodeTableStyled";
+import React from "react";
+
+export const SearchLabeledAutocomplete = ({ children, name, options, value, setFilterFields }) => {
+ function handleChange(_, value) {
+ setFilterFields((prev) => ({ ...prev, [name]: [value] }));
+ }
+
+ return (
+
+ {children}
+ (
+
+
+
+ )}
+ />
+
+ );
+};
+
+SearchLabeledAutocomplete.displayName = "SearchLabeledAutocomplete";
diff --git a/designer/client/src/components/sidePanels/SearchLabeledInput.tsx b/designer/client/src/components/sidePanels/SearchLabeledInput.tsx
index 23e5354edd9..96acec5c5b5 100644
--- a/designer/client/src/components/sidePanels/SearchLabeledInput.tsx
+++ b/designer/client/src/components/sidePanels/SearchLabeledInput.tsx
@@ -1,19 +1,18 @@
-import { forwardRef, PropsWithChildren } from "react";
+import React from "react";
import { FormControl } from "@mui/material";
import { nodeInput } from "../graph/node-modal/NodeDetailsContent/NodeTableStyled";
-import React from "react";
-export type SearchLabeledInputProps = PropsWithChildren<{
- name: string;
-}>;
+export const SearchLabeledInput = ({ children, name, value, setFilterFields }) => {
+ function handleChange(event) {
+ setFilterFields((prev) => ({ ...prev, [name]: event.target.value.split(",") }));
+ }
-export const SearchLabeledInput = forwardRef(({ children, ...props }, ref) => {
return (
{children}
-
+
);
-});
+};
SearchLabeledInput.displayName = "SearchLabeledInput";
diff --git a/designer/client/src/components/toolbars/search/AdvancedSearchFilters.tsx b/designer/client/src/components/toolbars/search/AdvancedSearchFilters.tsx
index 51f0f9bf713..ac33f20ee88 100644
--- a/designer/client/src/components/toolbars/search/AdvancedSearchFilters.tsx
+++ b/designer/client/src/components/toolbars/search/AdvancedSearchFilters.tsx
@@ -1,99 +1,85 @@
-import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
-import { Button, Box, Typography } from "@mui/material";
+import React, { useEffect, useMemo } from "react";
+import { Box, Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { SearchLabeledInput } from "../../sidePanels/SearchLabeledInput";
import { SearchLabel } from "../../sidePanels/SearchLabel";
+import { resolveSearchQuery, searchQueryToString, selectorByName } from "./utils";
import { SearchQuery } from "./SearchResults";
-import { resolveSearchQuery } from "./utils";
-
-const transformInput = (input: string, fieldName: string) => {
- return input === "" ? "" : `${fieldName}:(${input})`;
-};
-
-function extractSimpleSearchQuery(text: string): string {
- const regex = /(\w+):\(([^)]*)\)/g;
- let match: RegExpExecArray | null;
- let lastIndex = 0;
-
- while ((match = regex.exec(text)) !== null) {
- lastIndex = regex.lastIndex;
- }
-
- const rest = text.slice(lastIndex).trim();
-
- return rest;
-}
+import { SearchLabeledAutocomplete } from "../../sidePanels/SearchLabeledAutocomplete";
+import { useSelector } from "react-redux";
+import { getScenario } from "../../../reducers/selectors/graph";
+import NodeUtils from "../../graph/NodeUtils";
+import { uniq } from "lodash";
+import { getComponentGroups } from "../../../reducers/selectors/settings";
export function AdvancedSearchFilters({
+ filterFields,
+ setFilterFields,
filter,
setFilter,
setCollapsedHandler,
- refForm,
}: {
+ filterFields: SearchQuery;
+ setFilterFields: React.Dispatch>;
filter: string;
setFilter: React.Dispatch>;
setCollapsedHandler: React.Dispatch>;
- refForm: MutableRefObject;
}) {
const { t } = useTranslation();
- //const refForm = useRef(null);
+ const componentsGroups = useSelector(getComponentGroups);
+ const { scenarioGraph } = useSelector(getScenario);
+ const allNodes = NodeUtils.nodesFromScenarioGraph(scenarioGraph);
const displayNames = useMemo(
() => ({
- id: t("panels.search.field.id", "Name"),
+ name: t("panels.search.field.id", "Name"),
description: t("panels.search.field.description", "Description"),
type: t("panels.search.field.type", "Type"),
- paramName: t("panels.search.field.paramName", "Label"),
- paramValue: t("panels.search.field.paramValue", "Value"),
- outputValue: t("panels.search.field.outputValue", "Output"),
- edgeExpression: t("panels.search.field.edgeExpression", "Edge"),
+ label: t("panels.search.field.paramName", "Label"),
+ value: t("panels.search.field.paramValue", "Value"),
+ output: t("panels.search.field.outputValue", "Output"),
+ edge: t("panels.search.field.edgeExpression", "Edge"),
}),
[t],
);
- //Here be dragons: direct DOM manipulation
- useEffect(() => {
- if (refForm.current) {
- const searchQuery = resolveSearchQuery(filter);
- const formElements = refForm.current.elements;
+ const componentLabels = useMemo(() => {
+ return new Set(
+ componentsGroups.flatMap((componentGroup) => componentGroup.components).map((component) => component.label.toLowerCase()),
+ );
+ }, []);
+
+ const nodeTypes = useMemo(() => {
+ const availableTypes = allNodes
+ .flatMap((node) =>
+ selectorByName("type")
+ .flatMap((s) => s.selector(node))
+ .filter((item) => item !== undefined),
+ )
+ .map((selectorResult) => (typeof selectorResult === "string" ? selectorResult : selectorResult?.expression))
+ .filter((type) => componentLabels.has(type.toLowerCase()));
+
+ return uniq(availableTypes);
+ }, [allNodes]);
- Array.from(formElements).forEach((element: HTMLInputElement) => {
- if (element.name in searchQuery) {
- element.value = (searchQuery[element.name] || []).join(",");
- } else {
- element.value = "";
- }
- });
- }
+ useEffect(() => {
+ const searchQuery = resolveSearchQuery(filter);
+ setFilterFields(searchQuery);
}, [filter]);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
- const formData = new FormData(event.currentTarget);
-
- const transformedInputs = Array.from(formData.entries())
- .map(([fieldName, fieldValue]) => {
- const input = (fieldValue as string) || "";
- return transformInput(input, fieldName);
- })
- .filter((input) => input !== "");
-
- const finalText = transformedInputs.join(" ").trim() + " " + extractSimpleSearchQuery(filter);
-
- setFilter(finalText);
+ setFilter(searchQueryToString(filterFields));
setCollapsedHandler(false);
};
const handleClear = () => {
- setFilter(extractSimpleSearchQuery(filter));
-
- refForm.current.reset();
+ setFilter(filterFields?.plainQuery);
};
return (
{t("search.panel.advancedFilters.label", "Advanced Search")}
-
-
-
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
diff --git a/designer/client/src/components/toolbars/search/SearchPanel.tsx b/designer/client/src/components/toolbars/search/SearchPanel.tsx
index 1243ff60559..1e5f7528e77 100644
--- a/designer/client/src/components/toolbars/search/SearchPanel.tsx
+++ b/designer/client/src/components/toolbars/search/SearchPanel.tsx
@@ -5,20 +5,20 @@ import { AdvancedOptionsIcon, SearchIcon } from "../../table/SearchFilter";
import { Focusable } from "../../themed/InputWithIcon";
import { ToolbarPanelProps } from "../../toolbarComponents/DefaultToolbarPanel";
import { ToolbarWrapper } from "../../toolbarComponents/toolbarWrapper/ToolbarWrapper";
-import { SearchResults } from "./SearchResults";
+import { SearchQuery, SearchResults } from "./SearchResults";
import { SearchInputWithIcon } from "../../themed/SearchInput";
import { EventTrackingSelector, getEventTrackingProps } from "../../../containers/event-tracking";
import { Collapse } from "@mui/material";
import { AdvancedSearchFilters } from "./AdvancedSearchFilters";
-import { SearchPanelStyled, TipPanelStyled } from "../../tips/Styled";
+import { SearchPanelStyled } from "../../tips/Styled";
export function SearchPanel(props: ToolbarPanelProps): ReactElement {
const { t } = useTranslation();
+ const [filterFields, setFilterFields] = useState({});
const [filter, setFilter] = useState("");
- const refForm = useRef(null);
const clearFilter = useCallback(() => {
setFilter("");
- refForm.current.reset();
+ setFilterFields({});
}, []);
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(false);
@@ -44,7 +44,8 @@ export function SearchPanel(props: ToolbarPanelProps): ReactElement {
node.id,
},
{
@@ -24,30 +23,30 @@ const fieldsSelectors: FilterSelector = [
},
{
name: "type",
- selector: (node) => node.type,
+ selector: (node) => [node.nodeType, node.ref?.typ, node.ref?.id, node.type, node.service?.id],
},
{
- name: "paramValue",
+ name: "value",
selector: (node) => node.ref?.outputVariableNames && Object.values(node.ref?.outputVariableNames),
},
{
- name: "paramName",
+ name: "label",
selector: (node) => node.ref?.outputVariableNames && Object.keys(node.ref?.outputVariableNames),
},
{
- name: "paramValue",
+ name: "value",
selector: (node) => [node.expression, node.exprVal, node.value],
},
{
- name: "outputValue",
+ name: "output",
selector: (node) => [node.outputName, node.output, node.outputVar, node.varName],
},
{
- name: "paramValue",
+ name: "value",
selector: (node) => [node.parameters, node.ref?.parameters, node.service?.parameters, node.fields].flat().map((p) => p?.expression),
},
{
- name: "paramName",
+ name: "label",
selector: (node) => [node.parameters, node.ref?.parameters, node.service?.parameters, node.fields].flat().map((p) => p?.name),
},
];
@@ -57,7 +56,7 @@ function matchFilters(value: SelectorResult, filterValues: string[]): boolean {
return filterValues.length && filterValues.some((filter) => filter != "" && resolved?.toLowerCase().includes(filter.toLowerCase()));
}
-export const findFields = (filterValues: string[], node: NodeType) => {
+const findFields = (filterValues: string[], node: NodeType) => {
return uniq(
fieldsSelectors.flatMap(({ name, selector }) =>
ensureArray(selector(node))
@@ -69,16 +68,18 @@ export const findFields = (filterValues: string[], node: NodeType) => {
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),
- ),
+ selectorByName(selectorName).flatMap(({ name, selector }) =>
+ ensureArray(selector(node))
+ .filter((v) => matchFilters(v, filterValues))
+ .map(() => name),
+ ),
);
};
+export const selectorByName = (selectorName: string): FilterSelector => {
+ return fieldsSelectors.filter((selector) => selector.name == selectorName);
+};
+
export function useFilteredNodes(searchQuery: SearchQuery): {
groups: string[];
node: NodeType;
@@ -94,13 +95,13 @@ export function useFilteredNodes(searchQuery: SearchQuery): {
const displayNames = useMemo(
() => ({
- id: t("panels.search.field.id", "Name"),
+ name: t("panels.search.field.name", "Name"),
description: t("panels.search.field.description", "Description"),
+ label: t("panels.search.field.label", "Label"),
+ value: t("panels.search.field.value", "Value"),
+ output: t("panels.search.field.output", "Output"),
+ edge: t("panels.search.field.edge", "Edge"),
type: t("panels.search.field.type", "Type"),
- paramName: t("panels.search.field.paramName", "Label"),
- paramValue: t("panels.search.field.paramValue", "Value"),
- outputValue: t("panels.search.field.outputValue", "Output"),
- edgeExpression: t("panels.search.field.edgeExpression", "Edge"),
}),
[t],
);
@@ -118,7 +119,7 @@ export function useFilteredNodes(searchQuery: SearchQuery): {
.filter((e) => matchFilters(e.edgeType?.condition, [searchQuery.plainQuery]));
groups = findFields([searchQuery.plainQuery], node)
- .concat(edges.length ? "edgeExpression" : null)
+ .concat(edges.length ? "edge" : null)
.map((name) => displayNames[name])
.filter(Boolean);
@@ -129,15 +130,15 @@ export function useFilteredNodes(searchQuery: SearchQuery): {
.filter((e) => matchFilters(e.edgeType?.condition, [searchQuery.plainQuery]));
const groupsAux: string[] = findFields([searchQuery.plainQuery], node)
- .concat(edgesAux.length ? "edgeExpression" : null)
+ .concat(edgesAux.length ? "edge" : null)
.map((name) => displayNames[name])
.filter(Boolean);
edges =
- "edgeExpression" in searchQuery
+ "edge" in searchQuery
? allEdges
.filter((e) => e.from === node.id)
- .filter((e) => matchFilters(e.edgeType?.condition, searchQuery.edgeExpression))
+ .filter((e) => matchFilters(e.edgeType?.condition, searchQuery.edge))
: [];
const keyNamesRelevantForFiltering = Object.keys(searchQuery).filter(
@@ -151,7 +152,7 @@ export function useFilteredNodes(searchQuery: SearchQuery): {
groups = keyNamesRelevantForFiltering
.map((key) => findFieldsUsingSelectorWithName(key, searchQuery[key], node))
.flat()
- .concat(edges.length ? "edgeExpression" : null)
+ .concat(edges.length ? "edge" : null)
.map((name) => displayNames[name])
.filter(Boolean);
@@ -172,18 +173,7 @@ export function useFilteredNodes(searchQuery: SearchQuery): {
);
}
-export function resolveSearchQuery(filterRawText: string): SearchQuery {
- return parseRawTextToSearchQuery(filterRawText);
-}
-
-function splitString(input: string): string[] {
- //split string by comma respecting quoted elements
- //"a,b,c" -> ["a", "b", "c"]
- //"a,\"b,c\",d" -> ["a", "b,c", "d"]
- return input.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
-}
-
-function parseRawTextToSearchQuery(text: string): SearchQuery {
+export function resolveSearchQuery(text: string): SearchQuery {
const result: SearchQuery = {};
const regex = /(\w+):\(([^)]*)\)/g;
let match: RegExpExecArray | null;
@@ -200,3 +190,25 @@ function parseRawTextToSearchQuery(text: string): SearchQuery {
return result;
}
+
+export function searchQueryToString(query: SearchQuery): string {
+ const plainQuery = query.plainQuery;
+
+ const formattedParts = Object.entries(query)
+ .filter(([key]) => key !== "plainQuery")
+ .map(([key, value]) => {
+ if (Array.isArray(value) && !(value.length === 1 && value[0] === "")) {
+ return `${key}:(${value})`;
+ } else if (typeof value === "string" && value.length > 0) {
+ return `${key}:(${[value]})`;
+ }
+ return []; // Skip undefined or invalid values
+ })
+ .filter(Boolean) // Remove null values
+ .join(" "); // Join the formatted parts with a space
+
+ // Append plainQuery at the end without a key if it exists
+ return plainQuery
+ ? `${formattedParts} ${plainQuery}`.trim() // Ensure no trailing space
+ : formattedParts;
+}
diff --git a/designer/client/src/reducers/selectors/settings.ts b/designer/client/src/reducers/selectors/settings.ts
index ec562411fb4..e222a76b8ae 100644
--- a/designer/client/src/reducers/selectors/settings.ts
+++ b/designer/client/src/reducers/selectors/settings.ts
@@ -1,7 +1,7 @@
import { createSelector } from "reselect";
import { MetricsType } from "../../actions/nk";
import { DynamicTabData } from "../../containers/DynamicTab";
-import { ProcessDefinitionData } from "../../types";
+import { ComponentGroup, ProcessDefinitionData } from "../../types";
import { RootState } from "../index";
import { AuthenticationSettings, SettingsState } from "../settings";
import { uniqBy } from "lodash";
@@ -17,6 +17,7 @@ export const getSurveySettings = createSelector(getFeatureSettings, (s) => s?.su
export const getLoggedUser = createSelector(getSettings, (s) => s.loggedUser);
export const getLoggedUserId = createSelector(getLoggedUser, (s) => s.id);
export const getProcessDefinitionData = createSelector(getSettings, (s) => s.processDefinitionData || ({} as ProcessDefinitionData));
+export const getComponentGroups = createSelector(getProcessDefinitionData, (p) => p.componentGroups || ({} as ComponentGroup[]));
export const getCategories = createSelector(getLoggedUser, (u) => u.categories || []);
export const getWritableCategories = createSelector(getLoggedUser, getCategories, (user, categories) =>
categories.filter((c) => user.canWrite(c)),