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