diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md index 194c9be..6c82c21 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md @@ -15,10 +15,11 @@ propComponents: ['DataView'] sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md --- import { useMemo } from 'react'; -import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect'; import DataView from '@patternfly/react-data-view/dist/dynamic/DataView'; import DataViewToolbar from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; +import { FilterLabels } from '@patternfly/react-data-view/dist/dynamic/FilterLabels'; The **data view** component renders record data in a configured layout. diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/PredefinedLayoutExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/PredefinedLayoutExample.tsx index 2b8adcb..b7a1ea3 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/PredefinedLayoutExample.tsx +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/PredefinedLayoutExample.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import { Pagination } from '@patternfly/react-core'; +import { Pagination, SearchInput, ToolbarFilter } from '@patternfly/react-core'; import { Table, Tbody, Th, Thead, Tr, Td } from '@patternfly/react-table'; -import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { useDataViewFilters, useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect'; import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; @@ -43,6 +43,8 @@ export const BasicExample: React.FunctionComponent = () => { const { page, perPage } = pagination; const selection = useDataViewSelection({}); const { selected, onSelect, isSelected } = selection; + const filtersContext = useDataViewFilters({ name: '' }); + const { filters, onSetFilters, onDeleteFilters } = filtersContext; const pageData = useMemo(() => repositories.slice((page - 1) * perPage, ((page - 1) * perPage) + perPage), [ page, perPage ]); @@ -74,7 +76,21 @@ export const BasicExample: React.FunctionComponent = () => { itemCount={repositories.length} {...pagination} /> - } + } + search={ + onSetFilters(undefined, { name: '' })} + deleteChipGroup={(category) => onSetFilters(undefined, { name: '' })} + categoryName="name" + > + onSetFilters(e, { name: value })} + value={filters.name} + onClear={(e) => onSetFilters(e, { name: '' })} + /> + } /> diff --git a/packages/module/src/DataViewToolbar/DataViewToolbar.tsx b/packages/module/src/DataViewToolbar/DataViewToolbar.tsx index 7184875..dc48164 100644 --- a/packages/module/src/DataViewToolbar/DataViewToolbar.tsx +++ b/packages/module/src/DataViewToolbar/DataViewToolbar.tsx @@ -10,9 +10,11 @@ export interface DataViewToolbarProps extends PropsWithChildren { bulkSelect?: React.ReactNode; /** React component to display pagination */ pagination?: React.ReactNode; + /** React component to display search input */ + search?: React.ReactNode; } -export const DataViewToolbar: React.FC = ({ className, ouiaId = 'DataViewToolbar', bulkSelect, pagination, children, ...props }: DataViewToolbarProps) => ( +export const DataViewToolbar: React.FC = ({ className, ouiaId = 'DataViewToolbar', bulkSelect, search, pagination, children, ...props }: DataViewToolbarProps) => ( {bulkSelect && ( @@ -20,6 +22,11 @@ export const DataViewToolbar: React.FC = ({ className, oui {bulkSelect} )} + {search && ( + + {search} + + )} {pagination && ( {pagination} diff --git a/packages/module/src/FilterLabels/FilterLabels.tsx b/packages/module/src/FilterLabels/FilterLabels.tsx new file mode 100644 index 0000000..eabc6f4 --- /dev/null +++ b/packages/module/src/FilterLabels/FilterLabels.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { Badge, Button, ButtonVariant, Chip, ChipGroup, ChipGroupProps, ChipProps } from '@patternfly/react-core'; +import { createUseStyles } from 'react-jss'; +import clsx from 'clsx'; + +export interface FilterLabel extends Omit { + /** The text of the filter label that will be displayed */ + text: string; + /** Optional count associated with the filter label */ + count?: number; +} + +export interface FilterLabelGroup extends Omit { + /** An array of filter labels that belong to the category */ + labels: FilterLabel[]; +} + +export type FilterLabelsFilter = FilterLabel | FilterLabelGroup; + +export interface FilterLabelsProps { + /** Additional class names to be applied to the FilterLabels component */ + className?: string; + /** Array of filters that can be either individual labels or label groups */ + filters: FilterLabelsFilter[]; + /** Callback function invoked when one or multiple filters are removed */ + onDelete: (event: React.MouseEvent, filters: FilterLabelsFilter | FilterLabelsFilter[]) => void; + /** Custom title for the delete all button */ + deleteAllButtonTitle?: React.ReactNode; + /** Determines whether to show the delete all button */ + showDeleteAllButton?: boolean; + /** Determines whether to show the delete group button */ + showDeleteGroupButton?: boolean; + /** FilterLabels OUIA ID */ + ouiaId?: string | number; +} + +// Filter label group type guard +export const isFilterLabelGroup = (group: FilterLabelsFilter): group is FilterLabelGroup => Object.prototype.hasOwnProperty.call(group, 'categoryName'); + +// Plain filter label type guard +export const isFilterLabel = (group: FilterLabelsFilter): group is FilterLabel => !isFilterLabelGroup(group); + +const useStyles = createUseStyles({ + chipFilters: { + '& .pf-v5-c-chip-group:not(:last-child)': { + marginRight: 'var(--pf-v5-global--spacer--sm)', + }, + '& .pf-v5-c-chip .pf-v5-c-badge': { + marginLeft: 'var(--pf-v5-global--spacer--xs)', + }, + }, +}); + +export const FilterLabels: React.FunctionComponent = ({ + className, + filters, + onDelete, + deleteAllButtonTitle = 'Clear filters', + showDeleteAllButton = true, + showDeleteGroupButton = false, + ouiaId = 'FilterLabels' +}: FilterLabelsProps) => { + const classes = useStyles(); + const groups: FilterLabelGroup[] = filters.filter(isFilterLabelGroup); + + const groupedFilters = groups.map(({ labels, ...group }) => ( + 1 && { + isClosable: true, + onClick: (event) => { + event.stopPropagation(); + onDelete( + event, + { ...group, labels } + ); + }, + })} + > + {labels.map((label: FilterLabel) => ( + { + event.stopPropagation(); + onDelete(event, { ...group, labels: [ label ] }); + }} + > + {label.text} + {label.count && ( + + {label.count} + + )} + {label.badge} + + ))} + + )); + + const plainFilters = filters.filter(isFilterLabel); + + return ( + + {groupedFilters} + {plainFilters?.map((label) => ( + + { + event.stopPropagation(); + onDelete(event, [ label ]); + }} + > + {label.text} + {label.count && ( + + {label.count} + + )} + {label.badge} + + + ))} + {(showDeleteAllButton && filters.length > 0) && ( + + )} + + ); +}; + +export default FilterLabels; diff --git a/packages/module/src/FilterLabels/index.ts b/packages/module/src/FilterLabels/index.ts new file mode 100644 index 0000000..fd9d080 --- /dev/null +++ b/packages/module/src/FilterLabels/index.ts @@ -0,0 +1,2 @@ +export { default } from './FilterLabels'; +export * from './FilterLabels'; diff --git a/packages/module/src/Hooks/filters.test.tsx b/packages/module/src/Hooks/filters.test.tsx new file mode 100644 index 0000000..491e973 --- /dev/null +++ b/packages/module/src/Hooks/filters.test.tsx @@ -0,0 +1,3 @@ +import '@testing-library/jest-dom'; + + diff --git a/packages/module/src/Hooks/filters.ts b/packages/module/src/Hooks/filters.ts new file mode 100644 index 0000000..d697a92 --- /dev/null +++ b/packages/module/src/Hooks/filters.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from "react"; + +export interface UseDataViewFiltersProps { + /** Array of initially selected entries */ + initialFilters?: Record; +} + +export const useDataViewFilters = (props: UseDataViewFiltersProps) => { + const [ filters, setFilters ] = useState>(props.initialFilters ?? {}); + + const onSetFilters = (_event: React.FormEvent | undefined, newFilters: Record) => { + setFilters(prev => ({ ...prev, ...newFilters })); + } + + const onDeleteFilters = (_event: React.FormEvent | undefined, filtersToDelete: Record) => { + setFilters(prev => ({ ...filtersToDelete })); + } + + return { + filters, + onSetFilters, + onDeleteFilters + }; +}; diff --git a/packages/module/src/Hooks/index.ts b/packages/module/src/Hooks/index.ts index 4ed29ea..546a0da 100644 --- a/packages/module/src/Hooks/index.ts +++ b/packages/module/src/Hooks/index.ts @@ -1,2 +1,3 @@ export * from './pagination'; export * from './selection'; +export * from './filters'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index f87a6d0..c844636 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -1,6 +1,9 @@ // this file is autogenerated by generate-index.js, modifying it manually will have no effect export * from './Hooks'; +export { default as FilterLabels } from './FilterLabels'; +export * from './FilterLabels'; + export { default as DataViewToolbar } from './DataViewToolbar'; export * from './DataViewToolbar';