diff --git a/packages/ui/src/components/Catalog/BaseCatalog.tsx b/packages/ui/src/components/Catalog/BaseCatalog.tsx index c37c737fc..1bf7bfb40 100644 --- a/packages/ui/src/components/Catalog/BaseCatalog.tsx +++ b/packages/ui/src/components/Catalog/BaseCatalog.tsx @@ -9,6 +9,7 @@ interface BaseCatalogProps { tiles: ITile[]; catalogLayout: CatalogLayout; onTileClick?: (tile: ITile) => void; + onTagClick: (_event: unknown, value: string) => void; } export const BaseCatalog: FunctionComponent = (props) => { @@ -34,12 +35,16 @@ export const BaseCatalog: FunctionComponent = (props) => { {props.catalogLayout == CatalogLayout.List && ( - {props.tiles?.map((tile) => )} + {props.tiles?.map((tile) => ( + + ))} )} {props.catalogLayout == CatalogLayout.Gallery && ( - {props.tiles?.map((tile) => )} + {props.tiles?.map((tile) => ( + + ))} )} diff --git a/packages/ui/src/components/Catalog/Catalog.tsx b/packages/ui/src/components/Catalog/Catalog.tsx index aeaa5f406..2fc13536d 100644 --- a/packages/ui/src/components/Catalog/Catalog.tsx +++ b/packages/ui/src/components/Catalog/Catalog.tsx @@ -9,12 +9,15 @@ interface CatalogProps { onTileClick?: (tile: ITile) => void; } +const checkThatArrayContainsAllTags = (arr: string[], tags: string[]) => tags.every((v) => arr.includes(v)); + export const Catalog: FunctionComponent> = (props) => { const [searchTerm, setSearchTerm] = useState(''); const [groups, setGroups] = useState([]); const [activeGroup, setActiveGroup] = useState(getFirstActiveGroup(props.tiles)); const [activeLayout, setActiveLayout] = useState(CatalogLayout.Gallery); const [filteredTiles, setFilteredTiles] = useState([]); + const [filterTags, setFilterTags] = useState([]); useEffect(() => { setGroups(Object.keys(props.tiles)); @@ -22,17 +25,26 @@ export const Catalog: FunctionComponent> = (prop }, [props.tiles]); useEffect(() => { - setFilteredTiles( - props.tiles[activeGroup]?.filter((tile) => { - return ( - tile.name.toLowerCase().includes(searchTerm.toLowerCase()) || - tile.title.toLowerCase().includes(searchTerm.toLowerCase()) || - tile.description?.toLowerCase().includes(searchTerm.toLowerCase()) || - tile.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase())) - ); - }), - ); - }, [searchTerm, activeGroup, props.tiles]); + let toBeFiltered: ITile[] = []; + // filter by selected tags + toBeFiltered = filterTags + ? props.tiles[activeGroup]?.filter((tile) => { + return checkThatArrayContainsAllTags(tile.tags, filterTags); + }) + : props.tiles[activeGroup]; + // filter by search term ( name, description, tag ) + toBeFiltered = searchTerm + ? toBeFiltered?.filter((tile) => { + return ( + tile.name.toLowerCase().includes(searchTerm.toLowerCase()) || + tile.title.toLowerCase().includes(searchTerm.toLowerCase()) || + tile.description?.toLowerCase().includes(searchTerm.toLowerCase()) || + tile.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase())) + ); + }) + : toBeFiltered; + setFilteredTiles(toBeFiltered); + }, [searchTerm, activeGroup, props.tiles, filterTags]); const onFilterChange = useCallback((_event: unknown, value = '') => { setSearchTerm(value); @@ -45,6 +57,12 @@ export const Catalog: FunctionComponent> = (prop [props], ); + const onTagClick = useCallback((_event: unknown, value = '') => { + setFilterTags((previousFilteredTags) => { + return previousFilteredTags.includes(value) ? previousFilteredTags : [...previousFilteredTags, value]; + }); + }, []); + return ( <> > = (prop layouts={[CatalogLayout.Gallery, CatalogLayout.List]} activeGroup={activeGroup} activeLayout={activeLayout} + filterTags={filterTags} onChange={onFilterChange} setActiveGroup={setActiveGroup} setActiveLayout={setActiveLayout} + setFilterTags={setFilterTags} /> ); diff --git a/packages/ui/src/components/Catalog/CatalogFilter.tsx b/packages/ui/src/components/Catalog/CatalogFilter.tsx index 2be44a936..a22bf346d 100644 --- a/packages/ui/src/components/Catalog/CatalogFilter.tsx +++ b/packages/ui/src/components/Catalog/CatalogFilter.tsx @@ -1,4 +1,14 @@ -import { Form, FormGroup, Grid, GridItem, SearchInput, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; +import { + Form, + FormGroup, + Grid, + GridItem, + Label, + LabelGroup, + SearchInput, + ToggleGroup, + ToggleGroupItem, +} from '@patternfly/react-core'; import { FunctionComponent, useEffect, useRef } from 'react'; import { CatalogLayout } from './Catalog.models'; import { CatalogLayoutIcon } from './CatalogLayoutIcon'; @@ -10,9 +20,11 @@ interface CatalogFilterProps { layouts: CatalogLayout[]; activeGroup: string; activeLayout: CatalogLayout; + filterTags: string[]; onChange: (event: unknown, value?: string) => void; setActiveGroup: (group: string) => void; setActiveLayout: (layout: CatalogLayout) => void; + setFilterTags: (tags: string[]) => void; } export const CatalogFilter: FunctionComponent = (props) => { @@ -22,6 +34,10 @@ export const CatalogFilter: FunctionComponent = (props) => { inputRef.current?.focus(); }, []); + const onClose = (tag: string) => { + props.setFilterTags(props.filterTags.filter((savedTag) => savedTag !== tag)); + }; + return (
@@ -76,6 +92,13 @@ export const CatalogFilter: FunctionComponent = (props) => { + + {props.filterTags.map((tag, index) => ( + + ))} +
); }; diff --git a/packages/ui/src/components/Catalog/DataListItem.scss b/packages/ui/src/components/Catalog/DataListItem.scss index 6db8ea176..53309eb47 100644 --- a/packages/ui/src/components/Catalog/DataListItem.scss +++ b/packages/ui/src/components/Catalog/DataListItem.scss @@ -21,10 +21,6 @@ flex-flow: wrap; } - &__tags { - margin-right: 5px; - } - @media (min-width: 768px) { &__title-div-right { justify-content: right; diff --git a/packages/ui/src/components/Catalog/DataListItem.test.tsx b/packages/ui/src/components/Catalog/DataListItem.test.tsx index a482e61d6..bc0d71afd 100644 --- a/packages/ui/src/components/Catalog/DataListItem.test.tsx +++ b/packages/ui/src/components/Catalog/DataListItem.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { ITile } from './Catalog.models'; import { CatalogDataListItem } from './DataListItem'; @@ -15,8 +15,18 @@ describe('DataListItem', () => { }; it('renders correctly', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); + + it('calls onTagClick prop when clicked', () => { + const onTagClick = jest.fn(); + + const { getByTestId } = render(); + + fireEvent.click(getByTestId('tag-tag1')); + + expect(onTagClick).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/ui/src/components/Catalog/DataListItem.tsx b/packages/ui/src/components/Catalog/DataListItem.tsx index 8914dc41f..47b74de4a 100644 --- a/packages/ui/src/components/Catalog/DataListItem.tsx +++ b/packages/ui/src/components/Catalog/DataListItem.tsx @@ -5,16 +5,17 @@ import { DataListItemRow, Grid, GridItem, - Label, + LabelGroup, } from '@patternfly/react-core'; import { FunctionComponent } from 'react'; import { IconResolver } from '../IconResolver'; import { ITile } from './Catalog.models'; import './DataListItem.scss'; -import { getTagColor } from './tag-color-resolver'; +import { CatalogTag, CatalogTagsPanel } from './Tags'; interface ICatalogDataListItemProps { tile: ITile; + onTagClick: (_event: unknown, value: string) => void; } const titleElementOrder = { @@ -50,40 +51,28 @@ export const CatalogDataListItem: FunctionComponent = {props.tile.title} - {props.tile.headerTags?.map((tag, index) => ( - - ))} - {props.tile.version && ( - - )} + + {props.tile.headerTags?.map((tag, index) => ( + + ))} + {props.tile.version && ( + + )} +
- {props.tile.tags?.map((tag, index) => ( - - ))} +
diff --git a/packages/ui/src/components/Catalog/Tags/CatalogTag.tsx b/packages/ui/src/components/Catalog/Tags/CatalogTag.tsx new file mode 100644 index 000000000..91ec83b89 --- /dev/null +++ b/packages/ui/src/components/Catalog/Tags/CatalogTag.tsx @@ -0,0 +1,24 @@ +import { Label } from '@patternfly/react-core'; +import { FunctionComponent } from 'react'; +import { getTagColor } from './tag-color-resolver'; + +interface ICatalogTagProps { + tag: string; + className?: string; + variant?: 'outline' | 'filled'; + textMaxWidth?: string; +} + +export const CatalogTag: FunctionComponent = (props) => { + return ( + + ); +}; diff --git a/packages/ui/src/components/Catalog/Tags/CatalogTagsPanel.tsx b/packages/ui/src/components/Catalog/Tags/CatalogTagsPanel.tsx new file mode 100644 index 000000000..1a6aeb40b --- /dev/null +++ b/packages/ui/src/components/Catalog/Tags/CatalogTagsPanel.tsx @@ -0,0 +1,33 @@ +import { Label, LabelGroup } from '@patternfly/react-core'; +import { FunctionComponent } from 'react'; +import { getTagColor } from './tag-color-resolver'; + +interface ICatalogTagsPanelProps { + tags: string[]; + onTagClick: (_event: unknown, value: string) => void; +} + +export const CatalogTagsPanel: FunctionComponent = (props) => { + return ( + + {props.tags.map((tag) => ( + + ))} + + ); +}; diff --git a/packages/ui/src/components/Catalog/Tags/index.ts b/packages/ui/src/components/Catalog/Tags/index.ts new file mode 100644 index 000000000..5dbaf9150 --- /dev/null +++ b/packages/ui/src/components/Catalog/Tags/index.ts @@ -0,0 +1,3 @@ +export * from './tag-color-resolver'; +export * from './CatalogTag'; +export * from './CatalogTagsPanel'; diff --git a/packages/ui/src/components/Catalog/tag-color-resolver.ts b/packages/ui/src/components/Catalog/Tags/tag-color-resolver.ts similarity index 100% rename from packages/ui/src/components/Catalog/tag-color-resolver.ts rename to packages/ui/src/components/Catalog/Tags/tag-color-resolver.ts diff --git a/packages/ui/src/components/Catalog/Tile.scss b/packages/ui/src/components/Catalog/Tile.scss index eeab06213..22df712b5 100644 --- a/packages/ui/src/components/Catalog/Tile.scss +++ b/packages/ui/src/components/Catalog/Tile.scss @@ -18,6 +18,9 @@ &__title { text-align: center; + display: flex; + flex-flow: column; + align-items: center; } &__body { @@ -25,9 +28,4 @@ overflow: hidden; text-overflow: ellipsis; } - - &__tags { - margin-right: 1px; - margin-left: 1px; - } } diff --git a/packages/ui/src/components/Catalog/Tile.test.tsx b/packages/ui/src/components/Catalog/Tile.test.tsx index 4adebec58..56b329ad2 100644 --- a/packages/ui/src/components/Catalog/Tile.test.tsx +++ b/packages/ui/src/components/Catalog/Tile.test.tsx @@ -14,17 +14,21 @@ describe('Tile', () => { }; it('renders correctly', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - it('calls onClick prop when clicked', () => { + it('calls onClick and onTagClick prop when clicked', () => { const onClick = jest.fn(); - const { getByRole } = render(); + const onTagClick = jest.fn(); - fireEvent.click(getByRole('button')); + const { getByTestId } = render(); + + fireEvent.click(getByTestId('tile-tile-name')); + fireEvent.click(getByTestId('tag-tag1')); expect(onClick).toHaveBeenCalledTimes(1); + expect(onTagClick).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/ui/src/components/Catalog/Tile.tsx b/packages/ui/src/components/Catalog/Tile.tsx index 698b94316..3f39a24c3 100644 --- a/packages/ui/src/components/Catalog/Tile.tsx +++ b/packages/ui/src/components/Catalog/Tile.tsx @@ -1,13 +1,14 @@ -import { Card, CardBody, CardFooter, CardHeader, CardTitle, Label } from '@patternfly/react-core'; +import { Card, CardBody, CardFooter, CardHeader, CardTitle, LabelGroup } from '@patternfly/react-core'; import { FunctionComponent, PropsWithChildren, useCallback } from 'react'; import { IconResolver } from '../IconResolver'; import { ITile } from './Catalog.models'; +import { CatalogTag, CatalogTagsPanel } from './Tags'; import './Tile.scss'; -import { getTagColor } from './tag-color-resolver'; interface TileProps { tile: ITile; onClick: (tile: ITile) => void; + onTagClick: (_event: unknown, value: string) => void; } export const Tile: FunctionComponent> = (props) => { @@ -18,6 +19,7 @@ export const Tile: FunctionComponent> = (props) => return ( > = (props) => >
- {props.tile.headerTags?.map((tag, index) => ( - - ))} + + {props.tile.headerTags?.map((tag, index) => ( + + ))} +
{props.tile.title} + {props.tile.version && ( + + )} {props.tile.description} - - {props.tile.tags?.map((tag, index) => ( - - ))} - {props.tile.version && ( - - )} + + {/*
e.stopPropagation()}> */} + + {/*
*/}
); diff --git a/packages/ui/src/components/Catalog/__snapshots__/DataListItem.test.tsx.snap b/packages/ui/src/components/Catalog/__snapshots__/DataListItem.test.tsx.snap index 60d4077bd..a85222d06 100644 --- a/packages/ui/src/components/Catalog/__snapshots__/DataListItem.test.tsx.snap +++ b/packages/ui/src/components/Catalog/__snapshots__/DataListItem.test.tsx.snap @@ -36,45 +36,71 @@ exports[`DataListItem renders correctly 1`] = ` > tile-title - - - - header-tag1 - - - - - - - header-tag2 - - - - - - - 1.0 - - - +
  • + + + + header-tag1 + + + +
  • +
  • + + + + header-tag2 + + + +
  • +
  • + + + + 1.0 + + + +
  • + + +
    - - - - tag1 - - - - - - - tag2 - - - +
  • + + + + tag1 + + + +
  • +
  • + + + + tag2 + + + +
  • + +
    +
    @@ -50,32 +51,54 @@ exports[`Tile renders correctly 1`] = ` class="tile__icon" src="" /> - - - - header-tag1 - - - - - - - header-tag2 - - - +
  • + + + + header-tag1 + + + +
  • +
  • + + + + header-tag2 + + + +
  • + +
    +
    +
    `; diff --git a/packages/ui/src/components/Catalog/index.ts b/packages/ui/src/components/Catalog/index.ts index 4d16225c2..fe89d609d 100644 --- a/packages/ui/src/components/Catalog/index.ts +++ b/packages/ui/src/components/Catalog/index.ts @@ -3,4 +3,3 @@ export * from './CatalogLayoutIcon'; export * from './Catalog.models'; export * from './DataListItem'; export * from './Tile'; -export * from './tag-color-resolver';