From f9f086dcf178bfe0bf5df0c37dcab080bf54ac39 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Mon, 21 Aug 2023 16:13:07 +0200 Subject: [PATCH] feat: more powerful project search --- .../SearchSuggestions/SearchSuggestions.tsx | 6 +- .../ProjectFeatureToggles.tsx | 17 ++- .../{useSearch.test.ts => useSearch.test.tsx} | 113 ++++++++++++++++++ frontend/src/hooks/useSearch.ts | 32 +++-- 4 files changed, 155 insertions(+), 13 deletions(-) rename frontend/src/hooks/{useSearch.test.ts => useSearch.test.tsx} (73%) diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx index df9653db26..d6c451a99b 100644 --- a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx +++ b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx @@ -72,9 +72,9 @@ export const SearchSuggestions: VFC = ({ return { name: column.filterName, header: column.Header ?? column.filterName, - options: [...new Set(filterOptions)].sort((a, b) => - a.localeCompare(b) - ), + options: [...new Set(filterOptions)] + .filter((it: unknown) => it) + .sort((a, b) => a.localeCompare(b)), suggestedOption: filterOptions[randomRow] ?? `example-${column.filterName}`, values: getFilterValues( diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 4576b275ee..7aba057166 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -36,7 +36,7 @@ import { createLocalStorage } from 'utils/createLocalStorage'; import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog'; import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; -import { useSearch } from 'hooks/useSearch'; +import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch'; import { Search } from 'component/common/Search/Search'; import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle'; import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog'; @@ -234,6 +234,7 @@ export const ProjectFeatureToggles = ({ accessor: 'type', Cell: FeatureTypeCell, align: 'center', + filterName: 'type', maxWidth: 80, }, { @@ -265,6 +266,20 @@ export const ProjectFeatureToggles = ({ Cell: FeatureTagCell, width: 80, searchable: true, + filterName: 'tags', + filterBy( + row: IFeatureToggleListItem, + values: string[] + ) { + return includesFilter( + getColumnValues(this, row), + values + ); + }, + filterParsing(value: string) { + // only first tag from the list is added to search suggestions + return value.split('\n')[0]; + }, }, ] : []), diff --git a/frontend/src/hooks/useSearch.test.ts b/frontend/src/hooks/useSearch.test.tsx similarity index 73% rename from frontend/src/hooks/useSearch.test.ts rename to frontend/src/hooks/useSearch.test.tsx index 493654cfe6..6c8b0180e4 100644 --- a/frontend/src/hooks/useSearch.test.ts +++ b/frontend/src/hooks/useSearch.test.tsx @@ -3,7 +3,10 @@ import { getSearchTextGenerator, searchInFilteredData, filter, + useSearch, } from './useSearch'; +import { FC } from 'react'; +import { render, screen } from '@testing-library/react'; const columns = [ { @@ -310,3 +313,113 @@ describe('filter', () => { ]); }); }); + +const SearchData: FC<{ searchValue: string }> = ({ searchValue }) => { + const search = useSearch(columns, searchValue, data); + + return
{search.data.map(item => item.name).join(',')}
; +}; + +const SearchText: FC<{ searchValue: string }> = ({ searchValue }) => { + const search = useSearch(columns, searchValue, data); + + return
{search.getSearchText(searchValue)}
; +}; + +describe('Search and filter data', () => { + it('should filter single value', () => { + render(); + + screen.getByText('my-feature-toggle-3,my-feature-toggle-4'); + }); + + it('should filter multiple values', () => { + render(); + + screen.getByText('my-feature-toggle-3,my-feature-toggle-4'); + }); + + it('should filter multiple values with spaces', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3,my-feature-toggle-4'); + }); + + it('should handle multiple filters', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3'); + }); + + it('should handle multiple filters with long spaces', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3,my-feature-toggle-4'); + }); + + it('should handle multiple filters and search string in between', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3'); + }); + + it('should handle multiple filters and search string at the end', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3'); + }); + + it('should handle multiple filters and search string at the beginning', () => { + render( + + ); + + screen.getByText('my-feature-toggle-3'); + }); + + it('should return basic search text', () => { + render(); + + screen.getByText('toggle-3'); + }); + + it('should return advanced search text', () => { + render( + + ); + + screen.getByText('toggle-3'); + }); +}); diff --git a/frontend/src/hooks/useSearch.ts b/frontend/src/hooks/useSearch.ts index f15bf2fb64..122d7689b4 100644 --- a/frontend/src/hooks/useSearch.ts +++ b/frontend/src/hooks/useSearch.ts @@ -12,32 +12,37 @@ type IUseSearchOutput = { getSearchContext: () => IGetSearchContextOutput; }; +const normalizeSearchValue = (value: string) => + value.replaceAll(/\s*,\s*/g, ','); + export const useSearch = ( columns: any[], searchValue: string, data: T[] ): IUseSearchOutput => { const getSearchText = useCallback( - (value: string) => getSearchTextGenerator(columns)(value), + (value: string) => + getSearchTextGenerator(columns)(normalizeSearchValue(value)), [columns] ); + const normalizedSearchValue = normalizeSearchValue(searchValue); const getSearchContext = useCallback(() => { - return { data, searchValue, columns }; - }, [data, searchValue, columns]); + return { data, searchValue: normalizedSearchValue, columns }; + }, [data, normalizedSearchValue, columns]); const search = useMemo(() => { - if (!searchValue) return data; + if (!normalizedSearchValue) return data; - const filteredData = filter(columns, searchValue, data); + const filteredData = filter(columns, normalizedSearchValue, data); const searchedData = searchInFilteredData( columns, - getSearchText(searchValue), + getSearchText(normalizedSearchValue), filteredData ); return searchedData; - }, [columns, searchValue, data, getSearchText]); + }, [columns, normalizedSearchValue, data, getSearchText]); return { data: search, getSearchText, getSearchContext }; }; @@ -67,6 +72,7 @@ export const searchInFilteredData = ( searchValue: string, filteredData: T[] ) => { + const trimmedSearchValue = searchValue.trim(); const searchableColumns = columns.filter( column => column.searchable && column.accessor ); @@ -74,10 +80,13 @@ export const searchInFilteredData = ( return filteredData.filter(row => { return searchableColumns.some(column => { if (column.searchBy) { - return column.searchBy(row, searchValue); + return column.searchBy(row, trimmedSearchValue); } - return defaultSearch(getColumnValues(column, row), searchValue); + return defaultSearch( + getColumnValues(column, row), + trimmedSearchValue + ); }); }); }; @@ -85,6 +94,11 @@ export const searchInFilteredData = ( const defaultFilter = (fieldValue: string, values: string[]) => values.some(value => fieldValue?.toLowerCase() === value?.toLowerCase()); +export const includesFilter = (fieldValue: string, values: string[]) => + values.some(value => + fieldValue?.toLowerCase().includes(value?.toLowerCase()) + ); + const defaultSearch = (fieldValue: string, value: string) => fieldValue?.toLowerCase().includes(value?.toLowerCase());