Skip to content

Commit

Permalink
Add AutocompleteFilter
Browse files Browse the repository at this point in the history
Typeahead filter inspired by Console's AutocompleteInput.

Key features:
1. match the search pattern in the entire string (not only prefix)
2. reset the filter after selection - the selected item is visible in
   the chip list

Contract:
1. enum values passed as supportedValues param have id prop the same as
   the label prop
2. values in supportedValues are unique (no de-duplication similar
   to EnumFilter)

Reference-Url: http://v4-archive.patternfly.org/v4/components/select#typeahead
Reference-Url: https://github.com/openshift/console/blob/cf961054415464ba90959b614517903fc9b808a3/frontend/public/components/autocomplete.tsx#L22
Signed-off-by: Radoslaw Szwajkowski <[email protected]>
  • Loading branch information
rszwajko committed Nov 28, 2023
1 parent 00b6a69 commit 2fd64e4
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 0 deletions.
104 changes: 104 additions & 0 deletions packages/common/src/components/Filter/AutocompleteFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useState } from 'react';

import {
Select,
SelectOption,
SelectOptionObject,
SelectVariant,
ToolbarChip,
ToolbarFilter,
} from '@patternfly/react-core';

import { FilterTypeProps } from './types';

/**
* Typeahead filter inspired by Console's AutocompleteInput.

Check warning on line 15 in packages/common/src/components/Filter/AutocompleteFilter.tsx

View workflow job for this annotation

GitHub Actions / Run linter and tests

Unknown word: "Typeahead"
*
* **Key features**:<br>
* 1) match the search pattern in the entire string (not only prefix)<br>
* 2) reset the filter after selection - the selected item is visible in the chip list
*
* **Enum contract**:<br>
* 1) enum values passed as supportedValues param have id prop the same as the label prop.<br>
* 2) values in supportedValues are unique (no de-duplication similar to EnumFilter).
*
* [<img src="static/media/src/components-stories/assets/github-logo.svg"><i class="fi fi-brands-github">
* <font color="green">View component source on GitHub</font>](https://github.com/kubev2v/forklift-console-plugin/blob/main/packages/common/src/components/Filter/AutocompleteFilter.tsx)
*/
export const AutocompleteFilter = ({
selectedFilters = [],
onFilterUpdate,
supportedValues = [],
title,
placeholderLabel,
filterId,
showFilter = true,
}: FilterTypeProps) => {
const [isExpanded, setExpanded] = useState(false);
const validSupported = supportedValues.filter(({ label, id }) => id === label);
const validSelected = selectedFilters.filter(
(filterId) => validSupported.filter(({ id }) => id === filterId).length > 0,
);

const deleteFilter = (label: string | ToolbarChip | SelectOptionObject): void =>
onFilterUpdate(validSelected.filter((filterLabel) => filterLabel !== label));

const hasFilter = (label: string | SelectOptionObject): boolean =>
!!validSelected.find((filterLabel) => filterLabel === label);

const addFilter = (label: string | SelectOptionObject): void => {
if (typeof label === 'string') {
onFilterUpdate([...validSelected, label]);
}
};

const options = validSupported.map(({ label }) => <SelectOption key={label} value={label} />);

const onFilter = (_, textInput) => {
if (textInput === '') {
return options;
}

return options.filter((child) =>
child.props.value.toString().toLowerCase().includes(textInput.toLowerCase()),
);
};

return (
<ToolbarFilter
key={filterId}
chips={validSelected}
deleteChip={(category, option) => deleteFilter(option)}
deleteChipGroup={() => onFilterUpdate([])}
categoryName={title}
showToolbarItem={showFilter}
>
<Select
variant={SelectVariant.typeahead}
aria-label={placeholderLabel}
onSelect={(event, option, isPlaceholder) => {
if (isPlaceholder) {
return;
}
// behave as Console's AutocompleteInput used i.e. to filter pods by label
if (!hasFilter(option)) {
addFilter(option);
}
setExpanded(false);
}}
// intentionally keep the selections list empty
// the select should pretend to be stateless
// the selection is stored outside in a new chip
selections={[]}
placeholderText={placeholderLabel}
isOpen={isExpanded}
onToggle={setExpanded}
onFilter={onFilter}
shouldResetOnSelect={true}
>
{options}
</Select>
</ToolbarFilter>
);
};
1 change: 1 addition & 0 deletions packages/common/src/components/Filter/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @index(['./*', /__/g], f => `export * from '${f.path}';`)
export * from './AutocompleteFilter';
export * from './DateFilter';
export * from './DateRangeFilter';
export * from './EnumFilter';
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/components/FilterGroup/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import jsonpath from 'jsonpath';

import { areSameDayInUTCZero, isInClosedRange, ResourceField } from '../../utils';
import {
AutocompleteFilter,
DateFilter,
DateRangeFilter,
EnumFilter,
Expand Down Expand Up @@ -109,6 +110,11 @@ const searchableGroupedEnumMatcher = {
matchValue: enumMatcher.matchValue,
};

const autocompleteFilterMatcher = {
filterType: 'autocomplete',
matchValue: enumMatcher.matchValue,
};

const dateMatcher = {
filterType: 'date',
matchValue: (value: string) => (filter: string) => areSameDayInUTCZero(value, filter),
Expand All @@ -125,6 +131,7 @@ const sliderMatcher = {
};

export const defaultValueMatchers: ValueMatcher[] = [
autocompleteFilterMatcher,
freetextMatcher,
enumMatcher,
groupedEnumMatcher,
Expand All @@ -142,6 +149,7 @@ export const defaultSupportedFilters: Record<string, FilterRenderer> = {
groupedEnum: GroupedEnumFilter,
slider: SwitchFilter,
searchableGroupedEnum: SearchableGroupedEnumFilter,
autocomplete: AutocompleteFilter,
};

/**
Expand Down

0 comments on commit 2fd64e4

Please sign in to comment.