From d984547bc87156e0f0beda94e69a53f2a668a3ec Mon Sep 17 00:00:00 2001 From: Piotr Rudnicki Date: Mon, 2 Sep 2024 12:43:11 +0200 Subject: [PATCH] [Nu-1780] add ui --- .../client/src/assets/img/advanced-search.svg | 1 + .../src/components/table/SearchFilter.tsx | 27 ++++++ .../src/components/themed/InputWithIcon.tsx | 19 ++-- .../src/components/themed/SearchInput.tsx | 2 +- .../toolbars/search/AdvancedSearchOptions.tsx | 86 +++++++++++++++++++ .../toolbars/search/SearchPanel.tsx | 15 +++- .../src/components/toolbars/search/utils.ts | 14 ++- 7 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 designer/client/src/assets/img/advanced-search.svg create mode 100644 designer/client/src/components/toolbars/search/AdvancedSearchOptions.tsx diff --git a/designer/client/src/assets/img/advanced-search.svg b/designer/client/src/assets/img/advanced-search.svg new file mode 100644 index 00000000000..645aba94cfc --- /dev/null +++ b/designer/client/src/assets/img/advanced-search.svg @@ -0,0 +1 @@ + diff --git a/designer/client/src/components/table/SearchFilter.tsx b/designer/client/src/components/table/SearchFilter.tsx index 5941c62f254..a8f50fbaaed 100644 --- a/designer/client/src/components/table/SearchFilter.tsx +++ b/designer/client/src/components/table/SearchFilter.tsx @@ -1,6 +1,7 @@ import { css, cx } from "@emotion/css"; import React from "react"; import SearchSvg from "../../assets/img/search.svg"; +import AdvancedSearchSvg from "../../assets/img/advanced-search.svg"; import DeleteSvg from "../../assets/img/toolbarButtons/delete.svg"; import { useTheme } from "@mui/material"; @@ -9,6 +10,32 @@ const flex = css({ flex: 1, }); +export function AdvancedOptionsIcon(props: { + isActive?: boolean; + collapseHandler: React.Dispatch>; +}): JSX.Element { + const theme = useTheme(); + + const toggleCollapseHandler = () => { + props.collapseHandler((p) => !p); + }; + + return ( + + ); +} + export function SearchIcon(props: { isEmpty?: boolean }): JSX.Element { const theme = useTheme(); return ( diff --git a/designer/client/src/components/themed/InputWithIcon.tsx b/designer/client/src/components/themed/InputWithIcon.tsx index 033a46c578c..c4a4432b825 100644 --- a/designer/client/src/components/themed/InputWithIcon.tsx +++ b/designer/client/src/components/themed/InputWithIcon.tsx @@ -1,12 +1,13 @@ import { css, cx } from "@emotion/css"; -import { styled, useTheme } from "@mui/material"; -import React, { forwardRef, PropsWithChildren, ReactElement, useCallback, useImperativeHandle, useRef } from "react"; +import { Box, styled, useTheme } from "@mui/material"; +import React, { forwardRef, PropsWithChildren, ReactElement, ReactNode, useCallback, useImperativeHandle, useRef } from "react"; import { ClearIcon } from "../table/SearchFilter"; import { InputProps, ThemedInput } from "./ThemedInput"; type Props = PropsWithChildren & { onClear?: () => void; onAddonClick?: () => void; + additionalIcon?: ReactNode; }; export type Focusable = { @@ -23,6 +24,9 @@ export const InputWithIcon = forwardRef(function InputWithIcon const wrapperWithAddonStyles = css({ position: "relative", + display: "flex", + flexDirection: "row", + alignItems: "center", }); const addonWrapperStyles = css({ @@ -59,6 +63,11 @@ export const InputWithIcon = forwardRef(function InputWithIcon return (
+ {children && ( +
focus())}> + {children} +
+ )}
{!!props.value && onClear && ( @@ -66,11 +75,7 @@ export const InputWithIcon = forwardRef(function InputWithIcon
)} - {children && ( -
focus())}> - {children} -
- )} + {props.additionalIcon &&
{props.additionalIcon}
}
); diff --git a/designer/client/src/components/themed/SearchInput.tsx b/designer/client/src/components/themed/SearchInput.tsx index 9cbbfc4e3e5..a495d420c38 100644 --- a/designer/client/src/components/themed/SearchInput.tsx +++ b/designer/client/src/components/themed/SearchInput.tsx @@ -3,7 +3,7 @@ import { InputWithIcon } from "./InputWithIcon"; export const SearchInputWithIcon = styled(InputWithIcon)(({ theme }) => ({ ...theme.typography.body2, - width: "100%", + width: "75%", borderRadius: 0, height: "36px !important", color: theme.palette.text.secondary, diff --git a/designer/client/src/components/toolbars/search/AdvancedSearchOptions.tsx b/designer/client/src/components/toolbars/search/AdvancedSearchOptions.tsx new file mode 100644 index 00000000000..d6d1d6e73e4 --- /dev/null +++ b/designer/client/src/components/toolbars/search/AdvancedSearchOptions.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { TextField, Button, Box, Typography } from "@mui/material"; + +const transformInput = (input: string, fieldName: string) => { + return input === "" ? "" : `${fieldName}:(${input})`; +}; + +export function AdvancedSearchOptions({ + setFilter, + setCollapsedHandler, +}: { + setFilter: React.Dispatch>; + setCollapsedHandler: React.Dispatch>; +}) { + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const idInput = (formData.get("id") as string) || ""; + const descriptionInput = (formData.get("description") as string) || ""; + const typeInput = (formData.get("type") as string) || ""; + const paramNameInput = (formData.get("paramName") as string) || ""; + const paramValueInput = (formData.get("paramValue") as string) || ""; + const outputValueInput = (formData.get("outputValue") as string) || ""; + const edgeExpressionInput = (formData.get("edgeExpression") as string) || ""; + + const transformedIdInput = transformInput(idInput, "id"); + const transformedDescriptionInput = transformInput(descriptionInput, "description"); + const transformedTypeInput = transformInput(typeInput, "type"); + const transformedParamNameInput = transformInput(paramNameInput, "paramName"); + const transformedParamValueInput = transformInput(paramValueInput, "paramValue"); + const transformedOutputValueInput = transformInput(outputValueInput, "outputValue"); + const transformedEdgeExpressionInput = transformInput(edgeExpressionInput, "edgeExpression"); + + const transformedInputs = [ + transformedIdInput, + transformedDescriptionInput, + transformedTypeInput, + transformedParamNameInput, + transformedParamValueInput, + transformedOutputValueInput, + transformedEdgeExpressionInput, + ]; + + const finalText = transformedInputs.join(" ").trimEnd(); + + setFilter(finalText); + }; + + const handleCancel = () => { + setCollapsedHandler(false); + }; + + return ( + + + Advanced Search + + + + + + + + + + + + + + ); +} diff --git a/designer/client/src/components/toolbars/search/SearchPanel.tsx b/designer/client/src/components/toolbars/search/SearchPanel.tsx index 8a0a0322dfb..d72973ac0c0 100644 --- a/designer/client/src/components/toolbars/search/SearchPanel.tsx +++ b/designer/client/src/components/toolbars/search/SearchPanel.tsx @@ -1,26 +1,34 @@ import { isEmpty } from "lodash"; -import React, { ReactElement, useCallback, useRef, useState } from "react"; +import React, { ReactElement, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { SearchIcon } from "../../table/SearchFilter"; +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 { SearchInputWithIcon } from "../../themed/SearchInput"; import { EventTrackingSelector, getEventTrackingProps } from "../../../containers/event-tracking"; +import { Collapse } from "@mui/material"; +import { AdvancedSearchOptions } from "./AdvancedSearchOptions"; export function SearchPanel(props: ToolbarPanelProps): ReactElement { const { t } = useTranslation(); const [filter, setFilter] = useState(""); const clearFilter = useCallback(() => setFilter(""), []); + const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true); const searchRef = useRef(); + useEffect(() => { + setAdvancedOptionsCollapsed(false); + }, [filter]); + return ( searchRef.current?.focus()}> } onClear={clearFilter} value={filter} placeholder={t("panels.search.filter.placeholder", "type here to search nodes...")} @@ -28,6 +36,9 @@ export function SearchPanel(props: ToolbarPanelProps): ReactElement { > + + + ); diff --git a/designer/client/src/components/toolbars/search/utils.ts b/designer/client/src/components/toolbars/search/utils.ts index ca204948328..4d21872a245 100644 --- a/designer/client/src/components/toolbars/search/utils.ts +++ b/designer/client/src/components/toolbars/search/utils.ts @@ -187,6 +187,13 @@ export function resolveSearchOption(filterRawText: string): SearchOption { } } +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 parseRawTextToAdvancedSearch(text: string): AdvancedSearch { const result: AdvancedSearch = { searchType: SearchType.ADVANCED }; const regex = /(\w+):\(([^)]*)\)/g; @@ -194,10 +201,11 @@ function parseRawTextToAdvancedSearch(text: string): AdvancedSearch { while ((match = regex.exec(text)) !== null) { const key = match[1] as keyof Exclude; - console.log(key); - const values = match[2].split(",").map((value) => value.trim().replace(/"/g, "")); + console.log(match[2]); + const values = splitString(match[2]).map((value) => value.trim().replace(/"/g, "")); + //const values = match[2].split(",").map((value) => value.trim().replace(/"/g, "")); - result[key] = values.length > 0 ? values : undefined; + result[key] = values.length > 0 ? values : ""; } return result;