Skip to content

Commit

Permalink
[Nu-1780] add ui
Browse files Browse the repository at this point in the history
  • Loading branch information
Piotr Rudnicki committed Sep 10, 2024
1 parent 4fa77c2 commit c4694c8
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 111 deletions.
1 change: 1 addition & 0 deletions designer/client/src/assets/img/advanced-search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions designer/client/src/components/table/SearchFilter.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -9,6 +10,32 @@ const flex = css({
flex: 1,
});

export function AdvancedOptionsIcon(props: {
isActive?: boolean;
collapseHandler: React.Dispatch<React.SetStateAction<boolean>>;
}): JSX.Element {
const theme = useTheme();

const toggleCollapseHandler = () => {
props.collapseHandler((p) => !p);
};

return (
<AdvancedSearchSvg
onClick={toggleCollapseHandler}
className={cx(
flex,
css({
".icon-fill": {
fill: "none",
stroke: !props.isActive ? theme.palette.text.secondary : theme.palette.primary.main,
},
}),
)}
/>
);
}

export function SearchIcon(props: { isEmpty?: boolean }): JSX.Element {
const theme = useTheme();
return (
Expand Down
19 changes: 12 additions & 7 deletions designer/client/src/components/themed/InputWithIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<InputProps> & {
onClear?: () => void;
onAddonClick?: () => void;
endAdornment?: ReactNode;
};

export type Focusable = {
Expand All @@ -23,6 +24,9 @@ export const InputWithIcon = forwardRef<Focusable, Props>(function InputWithIcon

const wrapperWithAddonStyles = css({
position: "relative",
display: "flex",
flexDirection: "row",
alignItems: "center",
});

const addonWrapperStyles = css({
Expand Down Expand Up @@ -59,18 +63,19 @@ export const InputWithIcon = forwardRef<Focusable, Props>(function InputWithIcon

return (
<div className={cx(children && wrapperWithAddonStyles)}>
{children && (
<div className={addonStyles} onClick={onAddonClick ?? (() => focus())}>
{children}
</div>
)}
<ThemedInput ref={ref} {...props} />
<div className={addonWrapperStyles}>
{!!props.value && onClear && (
<div className={addonStyles} onClick={onClear}>
<ClearIcon />
</div>
)}
{children && (
<div className={addonStyles} onClick={onAddonClick ?? (() => focus())}>
{children}
</div>
)}
{props.endAdornment && <div className={addonStyles}>{props.endAdornment}</div>}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion designer/client/src/components/themed/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from "react";
import { TextField, Button, Box, Typography } from "@mui/material";

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

export function AdvancedSearchFilters({
filter,
setFilter,
setCollapsedHandler,
}: {
filter: string;
setFilter: React.Dispatch<React.SetStateAction<string>>;
setCollapsedHandler: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
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);
};

const handleCancel = () => {
setCollapsedHandler(false);
};

return (
<Box
component="form"
onSubmit={handleSubmit}
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "300px",
justifyContent: "center",
alignItems: "center",
}}
>
<Typography fontWeight="bold" sx={{ m: 1 }}>
Advanced Search
</Typography>
<TextField sx={{ m: 1 }} size="small" label="id" name="id" />
<TextField size="small" sx={{ m: 1 }} label="paramValue" name="paramValue" />
<TextField size="small" sx={{ m: 1 }} label="paramName" name="paramName" />
<TextField size="small" sx={{ m: 1 }} label="outputValue" name="outputValue" />
<TextField size="small" sx={{ m: 1 }} label="description" name="description" />
<TextField size="small" sx={{ m: 1 }} label="type" name="type" />
<TextField size="small" sx={{ m: 1 }} label="edgeExpression" name="edgeExpression" />
<Box sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", width: "85%", mt: 1, mb: 1 }}>
<Button sx={{ width: "45%" }} size="small" variant="outlined" onClick={handleCancel}>
Cancel
</Button>
<Button variant="contained" size="small" sx={{ width: "45%" }} type="submit">
Submit
</Button>
</Box>
</Box>
);
}
15 changes: 13 additions & 2 deletions designer/client/src/components/toolbars/search/SearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
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 { AdvancedSearchFilters } from "./AdvancedSearchFilters";

export function SearchPanel(props: ToolbarPanelProps): ReactElement {
const { t } = useTranslation();
const [filter, setFilter] = useState<string>("");
const clearFilter = useCallback(() => setFilter(""), []);
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(false);

const searchRef = useRef<Focusable>();

useEffect(() => {
setAdvancedOptionsCollapsed(false);
}, [filter]);

return (
<ToolbarWrapper {...props} title={t("panels.search.title", "Search")} onExpand={() => searchRef.current?.focus()}>
<SearchInputWithIcon
ref={searchRef}
onChange={setFilter}
endAdornment={<AdvancedOptionsIcon isActive={advancedOptionsCollapsed} collapseHandler={setAdvancedOptionsCollapsed} />}
onClear={clearFilter}
value={filter}
placeholder={t("panels.search.filter.placeholder", "type here to search nodes...")}
{...getEventTrackingProps({ selector: EventTrackingSelector.NodesInScenario })}
>
<SearchIcon isEmpty={isEmpty(filter)} />
</SearchInputWithIcon>
<Collapse in={advancedOptionsCollapsed} timeout="auto" unmountOnExit={false}>
<AdvancedSearchFilters filter={filter} setFilter={setFilter} setCollapsedHandler={setAdvancedOptionsCollapsed} />
</Collapse>
<SearchResults filterRawText={filter} />
</ToolbarWrapper>
);
Expand Down
28 changes: 6 additions & 22 deletions designer/client/src/components/toolbars/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,26 @@ 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 { resolveSearchOption, useFilteredNodes } from "./utils";
import { resolveSearchQuery, useFilteredNodes } from "./utils";
import { useGraph } from "../../graph/GraphContext";
import { nodeFound, nodeFoundHover } from "../../graph/graphStyledWrapper";
import { resetSelection } from "../../../actions/nk";
import { useWindows } from "../../../windowManager";

export enum SearchType {
SIMPLE,
ADVANCED,
}

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

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

export type SearchOption = SimpleSearch | AdvancedSearch;

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

const [hasFocus, setHasFocus] = useState(false);
const [hoveredNodes, setHoveredNodes] = useState<string[]>([]);
Expand Down Expand Up @@ -107,11 +95,7 @@ export function SearchResults({ filterRawText }: { filterRawText?: string }) {
disableGutters
divider
>
<FoundNode
node={node}
highlights={searchOption.searchType === SearchType.SIMPLE ? [searchOption.query] : searchOption.id}
fields={groups}
/>
<FoundNode node={node} highlights={[searchQuery.plainQuery]} fields={groups} />
</MenuItem>
))}
</MenuList>
Expand Down
Loading

0 comments on commit c4694c8

Please sign in to comment.