mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02: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