Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GroupedEnumFilter #90

Merged
merged 1 commit into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Filter/AttributeValueFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const AttributeValueFilter = ({
showFilter={currentFilter?.fieldId === id}
title={filter?.toLabel?.(t) ?? toFieldLabel(t)}
supportedValues={filter.values}
supportedGroups={filter.groups}
/>
)
);
Expand Down
9 changes: 8 additions & 1 deletion src/components/Filter/EnumFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,15 @@ export const useUnique = ({

/**
* Select one or many enum values from the list.
*
* Enum contract:
* 1) enum IDs(not translated identifiers) are required to be constant and unique within the enum
* 2) the translated labels might be duplicated (one label may map to multiple enum IDs).
* In such case enums with duplicated labels will be grouped as one option.
* The common scenario are values not known at the compile time represented by one label i.e. 'Unknown'.
*
* FilterTypeProps are interpeted as follows:
* 1) selectedFilters - selected enum IDs (not translated constant identifiers)
* 1) selectedFilters - selected enum IDs
* 2) onFilterUpdate - accepts the list of selected enum IDs
* 3) supportedValues - supported enum values
*/
Expand Down
131 changes: 131 additions & 0 deletions src/components/Filter/GroupedEnumFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { useTranslation } from 'src/utils/i18n';

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

import { EnumValue, FilterTypeProps } from './types';

/**
* Select one or many enum values.
*
* Enum contract:
* 1) values are grouped only for presentation and better user experience - logically it's one enum.
* 2) enum IDs(not translated identifiers) are required to be constant and unique within the enum
* 3) the translated labels are not checked for duplication and simply displayed.
* 4) groups are expected not to overlap (one item may belong to only one group)
* 5) items not assigned to any of the supported groups are skipped
*
*
* FilterTypeProps are interpeted as follows:
* 1) selectedFilters - selected enum IDs
* 2) onFilterUpdate - accepts the list of selected enum IDs
* 3) supportedValues - supported enum values
*/
export const GroupedEnumFilter = ({
selectedFilters: selectedEnumIds = [],
onFilterUpdate: onSelectedEnumIdsChange,
supportedValues: supportedEnumValues = [],
supportedGroups = [],
placeholderLabel,
showFilter,
}: FilterTypeProps) => {
const [isExpanded, setExpanded] = useState(false);
const { t } = useTranslation();

// simplify lookup
const id2enum = Object.fromEntries(
supportedEnumValues.map(({ id, ...rest }) => [id, { id, ...rest }]),
);

const deleteGroup = (groupId: string): void =>
onSelectedEnumIdsChange(
selectedEnumIds.filter((enumId) => id2enum?.[enumId]?.groupId !== groupId),
);

const deleteFilter = (id: string): void =>
onSelectedEnumIdsChange(selectedEnumIds.filter((enumId) => enumId !== id));

const hasFilter = (id: string): boolean =>
!!id2enum[id] && !!selectedEnumIds.find((enumId) => enumId === id);

const addFilter = (id: string): void => {
onSelectedEnumIdsChange([...selectedEnumIds, id]);
};

// put the IDs needed for compareTo (although not part of the interface)
const toSelectOption = ({ id, groupId, toLabel }): SelectOptionObject =>
({
toString: () => toLabel(t),
id,
groupId,
compareTo: (option) => option.id === id && option.groupId === groupId,
} as SelectOptionObject);

return (
<>
{/**
* use nested ToolbarFilter trick borrowed from the Openshift Console filter-toolbar:
* 1. one Select belongs to multiple ToolbarFilters.
* 2. each ToolbarFilter provides a different chip category
* 3. a chip category maps to group within the Select */}
{supportedGroups.reduce(
(acc, { toLabel, groupId }) => (
<ToolbarFilter
chips={selectedEnumIds
.map((id) => id2enum[id])
.filter((enumVal) => enumVal.groupId === groupId)
.map(({ id, toLabel }) => ({ key: id, node: toLabel(t) }))}
deleteChip={(category, option) => {
// values are one enum so id is enough to identify (category is not needed)
const id = typeof option === 'string' ? option : option.key;
deleteFilter(id);
}}
deleteChipGroup={(category) => {
const groupId = typeof category === 'string' ? category : category.key;
deleteGroup(groupId);
}}
categoryName={{ key: groupId, name: toLabel(t) }}
showToolbarItem={showFilter}
>
{acc}
</ToolbarFilter>
),
<Select
variant={SelectVariant.checkbox}
isGrouped
aria-label={placeholderLabel}
onSelect={(event, option, isPlaceholder) => {
if (isPlaceholder) {
return;
}
const id = typeof option === 'string' ? option : (option as EnumValue).id;
hasFilter(id) ? deleteFilter(id) : addFilter(id);
}}
selections={supportedEnumValues
.filter(({ id }) => selectedEnumIds.includes(id))
.map(toSelectOption)}
placeholderText={placeholderLabel}
isOpen={isExpanded}
onToggle={setExpanded}
>
{supportedGroups.map(({ toLabel, groupId }) => (
<SelectGroup key={groupId} label={toLabel(t)}>
{supportedEnumValues
.filter((item) => item.groupId === groupId)
.map(({ id, toLabel }) => (
<SelectOption key={id} value={toSelectOption({ id, toLabel, groupId })} />
))}
</SelectGroup>
))}
</Select>,
)}
</>
);
};
1 change: 1 addition & 0 deletions src/components/Filter/PrimaryFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const PrimaryFilters = ({
title={filter?.toLabel?.(t) ?? toFieldLabel(t)}
showFilter={true}
supportedValues={filter.values}
supportedGroups={filter.groups}
/>
)
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/Filter/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export * from './AttributeValueFilter';
export * from './EnumFilter';
export * from './FreetextFilter';
export * from './GroupedEnumFilter';
export * from './helpers';
export * from './matchers';
export * from './PrimaryFilters';
export * from './types';
7 changes: 6 additions & 1 deletion src/components/Filter/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ const enumMatcher = {
matchValue: (value: string) => (filter: string) => value === filter,
};

const defaultValueMatchers = [freetextMatcher, enumMatcher];
const groupedEnumMatcher = {
filterType: 'groupedEnum',
matchValue: enumMatcher.matchValue,
};

const defaultValueMatchers = [freetextMatcher, enumMatcher, groupedEnumMatcher];

/**
* Create matcher for multiple filter types.
Expand Down
23 changes: 18 additions & 5 deletions src/components/Filter/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
interface EnumGroup {
groupId: string;
toLabel(t: (key: string) => string): string;
}

export interface EnumValue {
id: string;
groupId?: string;
toLabel(t: (key: string) => string): string;
}

export interface FilterDef {
type: string;
toPlaceholderLabel(t: (key: string) => string): string;
values?: { id: string; toLabel(t: (key: string) => string): string }[];
values?: EnumValue[];
toLabel?(t: (key: string) => string): string;
primary?: boolean;
groups?: EnumGroup[];
}

/**
Expand All @@ -29,10 +41,11 @@ export interface FilterTypeProps {
/**
* (Optional) List of supported values (if limited)
*/
supportedValues?: {
id: string;
toLabel(t: (key: string) => string): string;
}[];
supportedValues?: EnumValue[];
/**
* (Optional) groups for supported values (if exist)
*/
supportedGroups?: EnumGroup[];
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/components/StandardPage/StandardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
AttributeValueFilter,
createMetaMatcher,
EnumFilter,
FilterTypeProps,
FreetextFilter,
GroupedEnumFilter,
PrimaryFilters,
toFieldFilter,
} from 'src/components/Filter';
import { FilterTypeProps } from 'src/components/Filter/types';
import { ManageColumnsToolbar, RowProps, TableView } from 'src/components/TableView';
import { Field } from 'src/components/types';
import { useTranslation } from 'src/utils/i18n';
Expand All @@ -24,7 +26,6 @@ import {
} from '@patternfly/react-core';
import { FilterIcon } from '@patternfly/react-icons';

import { toFieldFilter } from '../Filter/helpers';
import { useSort } from '../TableView/sort';

import { ErrorState, Loading, NoResultsFound, NoResultsMatchFilter } from './ResultStates';
Expand Down Expand Up @@ -100,6 +101,7 @@ export function StandardPage<T>({
supportedFilters = {
enum: EnumFilter,
freetext: FreetextFilter,
groupedEnum: GroupedEnumFilter,
},
customNoResultsFound,
customNoResultsMatchFilter,
Expand Down