diff --git a/react/src/components/BAIPropertyFilter.tsx b/react/src/components/BAIPropertyFilter.tsx index b7e1009c8..60d1a7beb 100644 --- a/react/src/components/BAIPropertyFilter.tsx +++ b/react/src/components/BAIPropertyFilter.tsx @@ -1,3 +1,4 @@ +import { filterEmptyItem } from '../helper'; import useControllableState from '../hooks/useControllableState'; import Flex from './Flex'; import { CloseCircleOutlined } from '@ant-design/icons'; @@ -34,6 +35,11 @@ type FilterProperty = { // TODO: support array, number type: 'string' | 'boolean'; options?: AutoCompleteProps['options']; + strictSelection?: boolean; + rule?: { + message: string; + validate: (value: string) => boolean; + }; }; export interface BAIPropertyFilterProps extends Omit, 'value' | 'onChange'> { @@ -74,10 +80,26 @@ const DEFAULT_OPTIONS_OF_TYPES: { string: undefined, }; +const DEFAULT_STRICT_SELECTION_OF_TYPES: { + [key: string]: boolean | undefined; +} = { + boolean: true, +}; + function trimFilterValue(filterValue: string): string { return filterValue.replace(/^%|%$/g, ''); } +export function mergeFilterValues( + filterStrings: Array, + operator: string = '&', +) { + return _.join( + _.map(filterEmptyItem(filterStrings), (str) => `(${str})`), + operator, + ); +} + /** * Parses the filter value and returns an object containing the property, operator, and value. * @param filter - The filter string to parse. @@ -101,6 +123,16 @@ export function parseFilterValue(filter: string) { return { property, operator, value }; } +/** + * Combines filter strings with the specified logical operator. + * @param filters - The array of filter strings to combine. + * @param operator - The logical operator to use ('and' or 'or'). + * @returns The combined filter string. + */ +function combineFilters(filters: string[], operator: 'and' | 'or'): string { + return filters.join(` ${operator} `); +} + const BAIPropertyFilter: React.FC = ({ filterProperties, value: propValue, @@ -150,22 +182,40 @@ const BAIPropertyFilter: React.FC = ({ const { token } = theme.useToken(); + const [isValid, setIsValid] = useState(true); + const [isFocused, setIsFocused] = useState(false); + useEffect(() => { if (list.length === 0) { setValue(undefined); } else { - setValue( - _.map(list, (item) => { - const valueStringInResult = - item.type === 'string' ? `"${item.value}"` : item.value; - return `${item.property} ${item.operator} ${valueStringInResult}`; - }).join(' & '), - ); + const filterStrings = _.map(list, (item) => { + const valueStringInResult = + item.type === 'string' ? `"${item.value}"` : item.value; + return `${item.property} ${item.operator} ${valueStringInResult}`; + }); + setValue(combineFilters(filterStrings, 'and')); // Change 'and' to 'or' if needed } }, [list, setValue]); const onSearch = (value: string) => { if (_.isEmpty(value)) return; + if ( + selectedProperty.strictSelection || + DEFAULT_STRICT_SELECTION_OF_TYPES[selectedProperty.type] + ) { + const option = _.find( + selectedProperty.options || + DEFAULT_OPTIONS_OF_TYPES[selectedProperty.type], + (o) => o.value === value, + ); + if (!option) return; + } + const isValid = + !selectedProperty.rule?.validate || selectedProperty.rule.validate(value); + setIsValid(isValid); + if (!isValid) return; + setSearch(''); const operator = selectedProperty.defaultOperator || @@ -194,38 +244,56 @@ const BAIPropertyFilter: React.FC = ({ onSelect={() => { autoCompleteRef.current?.focus(); setIsOpenAutoComplete(true); + setIsValid(true); }} showSearch optionFilterProp="label" /> - {}} - onSelect={onSearch} - onChange={(value) => { - setSearch(value); - }} - style={{ - minWidth: 200, - }} - // @ts-ignore - options={_.filter( - selectedProperty.options || - DEFAULT_OPTIONS_OF_TYPES[selectedProperty.type], - (option) => { - return _.isEmpty(search) - ? true - : option.label?.toString().includes(search); - }, - )} - placeholder={t('propertyFilter.placeHolder')} + - - + {}} + onSelect={onSearch} + onChange={(value) => { + setIsValid(true); + setSearch(value); + }} + style={{ + minWidth: 200, + }} + // @ts-ignore + options={_.filter( + selectedProperty.options || + DEFAULT_OPTIONS_OF_TYPES[selectedProperty.type], + (option) => { + return _.isEmpty(search) + ? true + : option.label?.toString().includes(search); + }, + )} + placeholder={t('propertyFilter.placeHolder')} + onBlur={() => { + setIsFocused(false); + }} + onFocus={() => { + setIsFocused(true); + }} + > + + + {list.length > 0 && (