From 0e162362e65baff6dfa206d4612e2c133e430687 Mon Sep 17 00:00:00 2001
From: Mateusz Kwasniewski
Date: Wed, 23 Aug 2023 09:38:10 +0200
Subject: [PATCH] feat: Change request advanced search and filter (#4544)
---
.../ChangeRequestsTabs/AvatarCell.tsx | 7 +-
.../ChangeRequestTitleCell.tsx | 5 +-
.../ChangeRequestsTabs/ChangeRequestsTabs.tsx | 33 +++++--
.../ChangeRequestsTabs/FeaturesCell.tsx | 15 +++-
.../SearchDescription/SearchDescription.tsx | 2 +-
.../SearchSuggestions.test.tsx | 89 +++++++++++++++++++
.../SearchSuggestions/SearchSuggestions.tsx | 21 +++--
7 files changed, 151 insertions(+), 21 deletions(-)
create mode 100644 frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.test.tsx
diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/AvatarCell.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/AvatarCell.tsx
index 24bfb149c5..10ae362ab5 100644
--- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/AvatarCell.tsx
+++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/AvatarCell.tsx
@@ -1,5 +1,7 @@
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { styled, Typography } from '@mui/material';
+import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
+import { Highlighter } from 'component/common/Highlighter/Highlighter';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
@@ -9,12 +11,15 @@ const StyledContainer = styled('div')(({ theme }) => ({
}));
export const AvatarCell = ({ value }: any) => {
+ const { searchQuery } = useSearchHighlightContext();
return (
{' '}
- {value?.username}
+
+ {value?.username}
+
diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestTitleCell.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestTitleCell.tsx
index a6e7105f83..2067a81a42 100644
--- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestTitleCell.tsx
+++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestTitleCell.tsx
@@ -2,6 +2,8 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Link, styled, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
+import { Highlighter } from 'component/common/Highlighter/Highlighter';
interface IChangeRequestTitleCellProps {
value?: any;
@@ -18,6 +20,7 @@ export const ChangeRequestTitleCell = ({
value,
row: { original },
}: IChangeRequestTitleCellProps) => {
+ const { searchQuery } = useSearchHighlightContext();
const projectId = useRequiredPathParam('projectId');
const {
id,
@@ -49,7 +52,7 @@ export const ChangeRequestTitleCell = ({
},
})}
>
- {title}
+ {title}
diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
index b48cf0a493..7d0a762782 100644
--- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
+++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx
@@ -3,13 +3,15 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
import {
SortableTableHeader,
Table,
+ TableBody,
TableCell,
TablePlaceholder,
+ TableRow,
} from 'component/common/Table';
import { SortingRule, useSortBy, useTable } from 'react-table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
-import { styled, Tab, Tabs, Box, useMediaQuery } from '@mui/material';
-import { Link } from 'react-router-dom';
+import { Box, styled, Tab, Tabs, useMediaQuery } from '@mui/material';
+import { Link, useSearchParams } from 'react-router-dom';
import { sortTypes } from 'utils/sortTypes';
import { useEffect, useMemo, useState } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@@ -17,17 +19,16 @@ import { Search } from 'component/common/Search/Search';
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
import theme from 'themes/theme';
import { useSearch } from 'hooks/useSearch';
-import { useSearchParams } from 'react-router-dom';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { ChangeRequestStatusCell } from './ChangeRequestStatusCell';
import { AvatarCell } from './AvatarCell';
import { ChangeRequestTitleCell } from './ChangeRequestTitleCell';
-import { TableBody, TableRow } from 'component/common/Table';
import { createLocalStorage } from 'utils/createLocalStorage';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { useStyles } from './ChangeRequestsTabs.styles';
import { FeaturesCell } from './FeaturesCell';
+import { HighlightCell } from '../../../common/Table/cells/HighlightCell/HighlightCell';
export interface IChangeRequestTableProps {
changeRequests: any[];
@@ -121,6 +122,21 @@ export const ChangeRequestsTabs = ({
Header: 'Updated feature toggles',
canSort: false,
accessor: 'features',
+ searchable: true,
+ filterName: 'feature',
+ filterParsing: (values: Array<{ name: string }>) => {
+ return values?.map(({ name }) => name).join('\n') || '';
+ },
+ filterBy: (
+ row: { features: Array<{ name: string }> },
+ values: Array
+ ) => {
+ return row.features.find(feature =>
+ values
+ .map(value => value.toLowerCase())
+ .includes(feature.name.toLowerCase())
+ );
+ },
Cell: ({
value,
row: {
@@ -141,11 +157,14 @@ export const ChangeRequestsTabs = ({
canSort: false,
Cell: AvatarCell,
align: 'left',
+ searchable: true,
+ filterName: 'by',
+ filterParsing: (value: { username?: string }) =>
+ value?.username || '',
},
{
Header: 'Submitted',
accessor: 'createdAt',
- searchable: true,
maxWidth: 100,
Cell: TimeAgoCell,
sortType: 'alphanumeric',
@@ -155,7 +174,8 @@ export const ChangeRequestsTabs = ({
accessor: 'environment',
searchable: true,
maxWidth: 100,
- Cell: TextCell,
+ Cell: HighlightCell,
+ filterName: 'environment',
},
{
Header: 'Status',
@@ -163,6 +183,7 @@ export const ChangeRequestsTabs = ({
searchable: true,
maxWidth: '170px',
Cell: ChangeRequestStatusCell,
+ filterName: 'status',
},
],
//eslint-disable-next-line
diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.tsx
index 60d664f816..2ee9a6cfbc 100644
--- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.tsx
+++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/FeaturesCell.tsx
@@ -1,8 +1,10 @@
import { Box, styled } from '@mui/material';
import { Link } from 'react-router-dom';
import { VFC } from 'react';
-import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
-import { TooltipLink } from '../../../common/TooltipLink/TooltipLink';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
+import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
+import { Highlighter } from 'component/common/Highlighter/Highlighter';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
@@ -42,6 +44,7 @@ interface FeaturesCellProps {
}
export const FeaturesCell: VFC = ({ value, project }) => {
+ const { searchQuery } = useSearchHighlightContext();
const featureNames = value?.map((feature: any) => feature.name);
return (
@@ -52,7 +55,9 @@ export const FeaturesCell: VFC = ({ value, project }) => {
title={featureName}
to={`/projects/${project}/features/${featureName}`}
>
- {featureName}
+
+ {featureName}
+
))}
elseShow={
@@ -65,7 +70,9 @@ export const FeaturesCell: VFC = ({ value, project }) => {
title={featureName}
to={`/projects/${project}/features/${featureName}`}
>
- {featureName}
+
+ {featureName}
+
))}
diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx
index 42003ea3a1..e2072b263b 100644
--- a/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx
+++ b/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx
@@ -61,7 +61,7 @@ export const SearchDescription: VFC = ({
{filter.values.join(',')}
{' '}
in {filter.header}. Options:{' '}
- {filter.options.join(', ')}
+ {filter.options.slice(0, 10).join(', ')}
))}
>
diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.test.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.test.tsx
new file mode 100644
index 0000000000..afc95200d4
--- /dev/null
+++ b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.test.tsx
@@ -0,0 +1,89 @@
+import { screen } from '@testing-library/react';
+import { render } from 'utils/testRenderer';
+import { SearchSuggestions } from './SearchSuggestions';
+
+const searchContext = {
+ data: [
+ {
+ title: 'Title A',
+ environment: 'prod',
+ },
+ {
+ title: 'Title B',
+ environment: 'dev env',
+ },
+ {
+ title: 'Title C',
+ environment: 'stage\npre-prod',
+ },
+ ],
+ searchValue: '',
+ columns: [
+ {
+ Header: 'Title',
+ searchable: true,
+ accessor: 'title',
+ },
+ {
+ Header: 'Environment',
+ accessor: 'environment',
+ searchable: false,
+ filterName: 'environment',
+ },
+ ],
+};
+
+test('displays search and filter instructions when no search value is provided', () => {
+ render( searchContext} />);
+
+ expect(
+ screen.getByText(/Filter your search with operators like:/i)
+ ).toBeInTheDocument();
+
+ expect(screen.getByText(/Filter by Environment:/i)).toBeInTheDocument();
+
+ expect(
+ screen.getByText(/environment:"dev env",pre-prod/i)
+ ).toBeInTheDocument();
+
+ expect(
+ screen.getByText(/Combine filters and search./i)
+ ).toBeInTheDocument();
+});
+
+test('displays search and filter instructions when search value is provided', () => {
+ render(
+ ({
+ ...searchContext,
+ searchValue: 'Title',
+ })}
+ />
+ );
+
+ expect(screen.getByText(/Searching for:/i)).toBeInTheDocument();
+ expect(screen.getByText(/in Title/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Combine filters and search./i)
+ ).toBeInTheDocument();
+});
+
+test('displays search and filter instructions when filter value is provided', () => {
+ render(
+ ({
+ ...searchContext,
+ searchValue: 'environment:prod',
+ })}
+ />
+ );
+
+ expect(screen.getByText(/Filtering by:/i)).toBeInTheDocument();
+ expect(screen.getByText(/in Environment/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Options: "dev env", pre-prod, prod, stage/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/Combine filters and search./i)
+ ).toBeInTheDocument();
+});
diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
index 20d3a6815b..1287ca5105 100644
--- a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
+++ b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx
@@ -11,8 +11,6 @@ import { useMemo, VFC } from 'react';
import { SearchDescription } from './SearchDescription/SearchDescription';
import { SearchInstructions } from './SearchInstructions/SearchInstructions';
-const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length);
-
const StyledPaper = styled(Paper)(({ theme }) => ({
position: 'absolute',
width: '100%',
@@ -53,6 +51,10 @@ interface SearchSuggestionsProps {
getSearchContext: () => IGetSearchContextOutput;
}
+const quote = (item: string) => (item.includes(' ') ? `"${item}"` : item);
+
+const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length);
+
export const SearchSuggestions: VFC = ({
getSearchContext,
}) => {
@@ -69,16 +71,19 @@ export const SearchSuggestions: VFC = ({
getColumnValues(column, row)
);
+ const options = [...new Set(filterOptions)]
+ .filter(Boolean)
+ .flatMap(item => item.split('\n'))
+ .filter(item => !item.includes('"') && !item.includes("'"))
+ .map(quote)
+ .sort((a, b) => a.localeCompare(b));
+
return {
name: column.filterName,
header: column.Header ?? column.filterName,
- options: [...new Set(filterOptions)]
- .filter(Boolean)
- .flatMap(item => item.split('\n'))
- .map(item => (item.includes(' ') ? `"${item}"` : item))
- .sort((a, b) => a.localeCompare(b)),
+ options,
suggestedOption:
- filterOptions[randomRow] ?? `example-${column.filterName}`,
+ options[randomRow] ?? `example-${column.filterName}`,
values: getFilterValues(
column.filterName,
searchContext.searchValue