mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: more powerful project search
This commit is contained in:
		
							parent
							
								
									2cfb99c768
								
							
						
					
					
						commit
						f9f086dcf1
					
				@ -72,9 +72,9 @@ export const SearchSuggestions: VFC<SearchSuggestionsProps> = ({
 | 
			
		||||
            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(
 | 
			
		||||
 | 
			
		||||
@ -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];
 | 
			
		||||
                          },
 | 
			
		||||
                      },
 | 
			
		||||
                  ]
 | 
			
		||||
                : []),
 | 
			
		||||
 | 
			
		||||
@ -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 <div>{search.data.map(item => item.name).join(',')}</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const SearchText: FC<{ searchValue: string }> = ({ searchValue }) => {
 | 
			
		||||
    const search = useSearch(columns, searchValue, data);
 | 
			
		||||
 | 
			
		||||
    return <div>{search.getSearchText(searchValue)}</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Search and filter data', () => {
 | 
			
		||||
    it('should filter single value', () => {
 | 
			
		||||
        render(<SearchData searchValue={'project:my-project'} />);
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3,my-feature-toggle-4');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter multiple values', () => {
 | 
			
		||||
        render(<SearchData searchValue={'project:my-project,another-value'} />);
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3,my-feature-toggle-4');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter multiple values with spaces', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData searchValue={'project:my-project  ,  another-value'} />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3,my-feature-toggle-4');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple filters', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData
 | 
			
		||||
                searchValue={'project:my-project ,another-value state:active'}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple filters with long spaces', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData
 | 
			
		||||
                searchValue={
 | 
			
		||||
                    'project:my-project   ,   another-value   state:active   ,   stale'
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3,my-feature-toggle-4');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple filters and search string in between', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData
 | 
			
		||||
                searchValue={
 | 
			
		||||
                    'project:my-project , another-value toggle-3 state:active , stale'
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple filters and search string at the end', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData
 | 
			
		||||
                searchValue={
 | 
			
		||||
                    'project:my-project , another-value state:active , stale toggle-3'
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle multiple filters and search string at the beginning', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchData
 | 
			
		||||
                searchValue={
 | 
			
		||||
                    'toggle-3 project:my-project , another-value state:active , stale'
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('my-feature-toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return basic search text', () => {
 | 
			
		||||
        render(<SearchText searchValue={'toggle-3'} />);
 | 
			
		||||
 | 
			
		||||
        screen.getByText('toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return advanced search text', () => {
 | 
			
		||||
        render(
 | 
			
		||||
            <SearchText
 | 
			
		||||
                searchValue={
 | 
			
		||||
                    'project:my-project , another-value toggle-3 state:active , stale'
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        screen.getByText('toggle-3');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -12,32 +12,37 @@ type IUseSearchOutput<T extends any> = {
 | 
			
		||||
    getSearchContext: () => IGetSearchContextOutput<T>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const normalizeSearchValue = (value: string) =>
 | 
			
		||||
    value.replaceAll(/\s*,\s*/g, ',');
 | 
			
		||||
 | 
			
		||||
export const useSearch = <T extends any>(
 | 
			
		||||
    columns: any[],
 | 
			
		||||
    searchValue: string,
 | 
			
		||||
    data: T[]
 | 
			
		||||
): IUseSearchOutput<T> => {
 | 
			
		||||
    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 = <T extends any>(
 | 
			
		||||
    searchValue: string,
 | 
			
		||||
    filteredData: T[]
 | 
			
		||||
) => {
 | 
			
		||||
    const trimmedSearchValue = searchValue.trim();
 | 
			
		||||
    const searchableColumns = columns.filter(
 | 
			
		||||
        column => column.searchable && column.accessor
 | 
			
		||||
    );
 | 
			
		||||
@ -74,10 +80,13 @@ export const searchInFilteredData = <T extends any>(
 | 
			
		||||
    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 = <T extends any>(
 | 
			
		||||
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());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user