Skip to content

Commit

Permalink
feat: strict selection mode of BAIPropertyFilter (#2931)
Browse files Browse the repository at this point in the history
**Changes:**
Added validation and error handling to the BAIPropertyFilter component:
- Introduced `strictSelection` property to enforce selecting only from provided options
- Added custom validation rules with error messages
- Implemented visual error states and tooltips for invalid inputs
- Added focus handling to control error message display
- Enhanced filter value handling with new merge and combine functions

**Rationale:**
These changes improve form validation and user feedback, ensuring data integrity by:
- Preventing invalid entries when strict selection is enabled
- Providing clear error messages through tooltips
- Supporting custom validation rules per property
- Maintaining consistent error states during user interaction
  • Loading branch information
yomybaby committed Dec 10, 2024
1 parent 7794961 commit 937ceac
Showing 1 changed file with 102 additions and 34 deletions.
136 changes: 102 additions & 34 deletions react/src/components/BAIPropertyFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { filterEmptyItem } from '../helper';
import useControllableState from '../hooks/useControllableState';
import Flex from './Flex';
import { CloseCircleOutlined } from '@ant-design/icons';
Expand Down Expand Up @@ -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<ComponentProps<typeof Flex>, 'value' | 'onChange'> {
Expand Down Expand Up @@ -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<string | undefined>,
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.
Expand All @@ -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<BAIPropertyFilterProps> = ({
filterProperties,
value: propValue,
Expand Down Expand Up @@ -150,22 +182,40 @@ const BAIPropertyFilter: React.FC<BAIPropertyFilterProps> = ({

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 ||
Expand Down Expand Up @@ -194,38 +244,56 @@ const BAIPropertyFilter: React.FC<BAIPropertyFilterProps> = ({
onSelect={() => {
autoCompleteRef.current?.focus();
setIsOpenAutoComplete(true);
setIsValid(true);
}}
showSearch
optionFilterProp="label"
/>
<AutoComplete
ref={autoCompleteRef}
value={search}
open={isOpenAutoComplete}
onDropdownVisibleChange={setIsOpenAutoComplete}
// https://ant.design/components/auto-complete#why-doesnt-the-text-composition-system-work-well-with-onsearch-in-controlled-mode
// onSearch={(value) => {}}
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')}
<Tooltip
title={isValid || !isFocused ? '' : selectedProperty.rule?.message}
open={!isValid && isFocused}
color={token.colorError}
>
<Input.Search onSearch={onSearch} allowClear />
</AutoComplete>
<AutoComplete
ref={autoCompleteRef}
value={search}
open={isOpenAutoComplete}
onDropdownVisibleChange={setIsOpenAutoComplete}
// https://ant.design/components/auto-complete#why-doesnt-the-text-composition-system-work-well-with-onsearch-in-controlled-mode
// onSearch={(value) => {}}
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);
}}
>
<Input.Search
onSearch={onSearch}
allowClear
status={!isValid && isFocused ? 'error' : undefined}
/>
</AutoComplete>
</Tooltip>
</Space.Compact>
{list.length > 0 && (
<Flex
Expand Down

0 comments on commit 937ceac

Please sign in to comment.