Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { ConsoleSelect } from '@console/internal/components/utils/console-select';
import { useDebounceCallback } from '@console/shared';
import { NO_GROUPING } from '../utils/category-utils';
import { /* CatalogSortOrder, */ CatalogStringMap } from '../utils/types';
import { CatalogSortOrder, CatalogStringMap } from '../utils/types';
import CatalogPageHeader from './CatalogPageHeader';
import CatalogPageHeading from './CatalogPageHeading';
import CatalogPageNumItems from './CatalogPageNumItems';
Expand All @@ -15,12 +15,12 @@ type CatalogToolbarProps = {
title: string;
totalItems: number;
searchKeyword: string;
// sortOrder: CatalogSortOrder;
sortOrder: CatalogSortOrder;
groupings: CatalogStringMap;
activeGrouping: string;
onGroupingChange: (grouping: string) => void;
onSearchKeywordChange: (searchKeyword: string) => void;
// onSortOrderChange: (sortOrder: CatalogSortOrder) => void;
onSortOrderChange: (sortOrder: CatalogSortOrder) => void;
};

const CatalogToolbar = forwardRef<HTMLInputElement, CatalogToolbarProps>(
Expand All @@ -29,22 +29,22 @@ const CatalogToolbar = forwardRef<HTMLInputElement, CatalogToolbarProps>(
title,
totalItems,
searchKeyword,
// sortOrder,
sortOrder,
groupings,
activeGrouping,
onGroupingChange,
onSearchKeywordChange,
// onSortOrderChange,
onSortOrderChange,
},
inputRef,
) => {
const { t } = useTranslation();

// TODO: Add sort order back in with a new sort by "Relevance" selection, that is the default sort order
// const catalogSortItems = {
// [CatalogSortOrder.ASC]: t('console-shared~A-Z'),
// [CatalogSortOrder.DESC]: t('console-shared~Z-A'),
// };
const catalogSortItems = {
[CatalogSortOrder.RELEVANCE]: t('console-shared~Relevance'),
[CatalogSortOrder.ASC]: t('console-shared~A-Z'),
[CatalogSortOrder.DESC]: t('console-shared~Z-A'),
};

const showGrouping = !_.isEmpty(groupings);

Expand Down Expand Up @@ -72,15 +72,16 @@ const CatalogToolbar = forwardRef<HTMLInputElement, CatalogToolbarProps>(
aria-label={t('console-shared~Filter by keyword...')}
/>
</FlexItem>
{/* <FlexItem>
<FlexItem>
<ConsoleSelect
className="co-catalog-page__sort"
items={catalogSortItems}
title={catalogSortItems[sortOrder]}
alwaysShowTitle
onChange={onSortOrderChange}
selectedKey={sortOrder}
/>
</FlexItem> */}
</FlexItem>
{showGrouping && (
<FlexItem>
<ConsoleSelect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
CatalogFilterGroupMap,
CatalogFilters as FiltersType,
CatalogQueryParams,
// CatalogSortOrder,
CatalogSortOrder,
CatalogStringMap,
CatalogType,
CatalogTypeCounts,
Expand Down Expand Up @@ -80,8 +80,9 @@ const CatalogView: React.FC<CatalogViewProps> = ({
const activeCategoryId = queryParams.get(CatalogQueryParams.CATEGORY) ?? ALL_CATEGORY;
const activeSearchKeyword = queryParams.get(CatalogQueryParams.KEYWORD) ?? '';
const activeGrouping = queryParams.get(CatalogQueryParams.GROUPING) ?? NO_GROUPING;
// const sortOrder =
// (queryParams.get(CatalogQueryParams.SORT_ORDER) as CatalogSortOrder) ?? CatalogSortOrder.ASC;
const sortOrder =
(queryParams.get(CatalogQueryParams.SORT_ORDER) as CatalogSortOrder) ??
CatalogSortOrder.RELEVANCE;
const activeFilters = React.useMemo(() => {
const attributeFilters = {};

Expand Down Expand Up @@ -139,9 +140,9 @@ const CatalogView: React.FC<CatalogViewProps> = ({
updateURLParams(CatalogQueryParams.GROUPING, grouping);
}, []);

// const handleSortOrderChange = React.useCallback((order) => {
// updateURLParams(CatalogQueryParams.SORT_ORDER, order);
// }, []);
const handleSortOrderChange = React.useCallback((order) => {
updateURLParams(CatalogQueryParams.SORT_ORDER, order);
}, []);

const handleShowAllToggle = React.useCallback((groupName) => {
setFilterGroupsShowAll((showAll) => {
Expand Down Expand Up @@ -175,6 +176,7 @@ const CatalogView: React.FC<CatalogViewProps> = ({
const filteredBySearchItems = filterBySearchKeyword(
filteredByCategoryItems,
activeSearchKeyword,
sortOrder,
);
const filteredByAttributes = filterByAttributes(filteredBySearchItems, activeFilters);

Expand All @@ -185,7 +187,7 @@ const CatalogView: React.FC<CatalogViewProps> = ({
setCatalogTypeCounts(typeCounts);

// Console table for final filtered results (only for operators)
if (filteredByAttributes.length > 0 && filteredByAttributes[0]?.type === 'operator') {
if (filteredByAttributes.length > 0) {
// Check if we have active filters beyond just search and category
const hasAttributeFilters = Object.values(activeFilters).some((filterGroup) =>
Object.values(filterGroup).some((filter) => filter.active),
Expand Down Expand Up @@ -261,6 +263,7 @@ const CatalogView: React.FC<CatalogViewProps> = ({
categorizedIds,
filterGroups,
items,
sortOrder,
]);

const totalItems = filteredItems.length;
Expand Down Expand Up @@ -335,11 +338,11 @@ const CatalogView: React.FC<CatalogViewProps> = ({
title={activeCategory.label}
totalItems={totalItems}
searchKeyword={activeSearchKeyword}
// sortOrder={sortOrder}
sortOrder={sortOrder}
groupings={groupings}
activeGrouping={activeGrouping}
onGroupingChange={handleGroupingChange}
// onSortOrderChange={handleSortOrderChange}
onSortOrderChange={handleSortOrderChange}
onSearchKeywordChange={handleSearchKeywordChange}
/>
{totalItems > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
keywordCompare,
calculateCatalogItemRelevanceScore,
getRedHatPriority,
sortCatalogItems,
} from '../catalog-utils';
import { CatalogSortOrder } from '../types';

// Mock CatalogItem data for testing
const createMockCatalogItem = (overrides: Partial<CatalogItem> = {}): CatalogItem => ({
Expand Down Expand Up @@ -313,3 +315,154 @@ describe('keywordCompare', () => {
expect(result[1].attributes?.provider).toBe('Other Company');
});
});

describe('sortCatalogItems', () => {
const testItems = [
createMockCatalogItem({
uid: 'item-1',
name: 'Zebra Operator',
attributes: { provider: 'Red Hat' },
description: 'Contains gitops in description',
}),
createMockCatalogItem({
uid: 'item-2',
name: 'Argo CD',
attributes: { provider: 'CNCF' },
description: 'GitOps tool for Kubernetes',
tags: ['gitops'],
}),
createMockCatalogItem({
uid: 'item-3',
name: 'Database Manager',
attributes: { provider: 'Red Hat' },
description: 'Database management',
}),
createMockCatalogItem({
uid: 'item-4',
name: 'Flux',
attributes: { provider: 'Weaveworks' },
description: 'GitOps for Kubernetes',
tags: ['gitops'],
}),
];

describe('filtering behavior', () => {
it('filters items by search keyword before sorting', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.ASC, 'gitops');

// Should only return items matching 'gitops'
expect(result).toHaveLength(3);
expect(result.every((item) => item.name !== 'Database Manager')).toBe(true);
});

it('returns all items when no search keyword provided', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.ASC, '');

expect(result).toHaveLength(4);
});

it('returns empty array when no items match search keyword', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.ASC, 'nonexistent');

expect(result).toHaveLength(0);
});
});

describe('RELEVANCE mode', () => {
it('delegates to keywordCompare correctly', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.RELEVANCE, 'gitops');

// Red Hat item should be first due to priority + relevance
expect(result[0].name).toBe('Zebra Operator');
expect(result[0].attributes?.provider).toBe('Red Hat');
});

it('uses keywordCompare with no search term', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.RELEVANCE, '');

// Should return all items sorted by Red Hat priority + alphabetical
expect(result).toHaveLength(4);
});
});

describe('A-Z mode', () => {
it('sorts alphabetically ascending without Red Hat priority', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.ASC, 'gitops');

// Pure alphabetical order (Argo CD, Flux, Zebra Operator)
expect(result[0].name).toBe('Argo CD');
expect(result[1].name).toBe('Flux');
expect(result[2].name).toBe('Zebra Operator');
});

it('sorts all items alphabetically when no search term', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.ASC, '');

// Pure alphabetical: Argo CD, Database Manager, Flux, Zebra Operator
expect(result[0].name).toBe('Argo CD');
expect(result[1].name).toBe('Database Manager');
expect(result[2].name).toBe('Flux');
expect(result[3].name).toBe('Zebra Operator');
});

it('ignores Red Hat priority in alphabetical sorting', () => {
const items = [
createMockCatalogItem({
name: 'Zebra Service',
attributes: { provider: 'Red Hat' }, // High priority
}),
createMockCatalogItem({
name: 'Alpha Service',
attributes: { provider: 'Community' }, // Low priority
}),
];

const result = sortCatalogItems(items, CatalogSortOrder.ASC, '');

// Alpha should come before Zebra despite lower Red Hat priority
expect(result[0].name).toBe('Alpha Service');
expect(result[1].name).toBe('Zebra Service');
});
});

describe('Z-A mode', () => {
it('sorts alphabetically descending without Red Hat priority', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.DESC, 'gitops');

// Reverse alphabetical order
expect(result[0].name).toBe('Zebra Operator');
expect(result[1].name).toBe('Flux');
expect(result[2].name).toBe('Argo CD');
});

it('sorts all items reverse alphabetically when no search term', () => {
const result = sortCatalogItems(testItems, CatalogSortOrder.DESC, '');

expect(result[0].name).toBe('Zebra Operator');
expect(result[1].name).toBe('Flux');
expect(result[2].name).toBe('Database Manager');
expect(result[3].name).toBe('Argo CD');
});
});

describe('edge cases', () => {
it('handles empty items array', () => {
const result = sortCatalogItems([], CatalogSortOrder.ASC, 'test');

expect(result).toEqual([]);
});

it('handles null/undefined items', () => {
const result = sortCatalogItems(null as any, CatalogSortOrder.ASC, 'test');

expect(result).toBeNull();
});

it('defaults to RELEVANCE mode when no sortOrder provided', () => {
const result = sortCatalogItems(testItems, undefined as any, 'gitops');

// Should behave like RELEVANCE mode
expect(result[0].name).toBe('Zebra Operator'); // Red Hat priority
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { normalizeIconClass } from '@console/internal/components/catalog/catalog-item-icon';
import { history } from '@console/internal/components/utils';
import catalogImg from '@console/internal/imgs/logos/catalog-icon.svg';
import { CatalogType, CatalogTypeCounts } from './types';
import { CatalogSortOrder, CatalogType, CatalogTypeCounts } from './types';

enum CatalogVisibilityState {
Enabled = 'Enabled',
Expand Down Expand Up @@ -208,6 +208,57 @@ export const keywordCompare = (filterString: string, items: CatalogItem[]): Cata
return sortedItems.map(({ relevanceScore, redHatPriority, ...item }) => item);
};

/**
* Sort catalog items based on the selected sort order.
* @param items - Array of catalog items to sort
* @param sortOrder - Sort order: RELEVANCE (default), ASC (A-Z), or DESC (Z-A)
* @param searchKeyword - Optional search keyword for filtering and relevance scoring
* @returns Sorted and filtered array of catalog items
*/
export const sortCatalogItems = (
items: CatalogItem[],
sortOrder: CatalogSortOrder = CatalogSortOrder.RELEVANCE,
searchKeyword = '',
): CatalogItem[] => {
if (!items || items.length === 0) {
return items;
}

// First, filter items by search keyword if provided
let filteredItems = items;
if (searchKeyword) {
const searchTerm = searchKeyword.toLowerCase();
filteredItems = items.filter((item) => {
const relevanceScore = calculateCatalogItemRelevanceScore(searchTerm, item);
return relevanceScore > 0;
});
}

// Then, sort the filtered items based on the selected sort order
switch (sortOrder) {
case CatalogSortOrder.RELEVANCE:
// Use the existing keywordCompare function for relevance-based sorting
// Note: keywordCompare handles its own filtering, so we pass the original items
return keywordCompare(searchKeyword, items);

case CatalogSortOrder.ASC:
// Sort alphabetically A-Z (pure alphabetical, no Red Hat prioritization)
return [...filteredItems].sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});

case CatalogSortOrder.DESC:
// Sort alphabetically Z-A (pure alphabetical, no Red Hat prioritization)
return [...filteredItems].sort((a, b) => {
return b.name.toLowerCase().localeCompare(a.name.toLowerCase());
});

default:
// Fallback to relevance sorting
return keywordCompare(searchKeyword, items);
}
};

export const getIconProps = (item: CatalogItem) => {
const { icon } = item;
if (!icon) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import { CatalogItem } from '@console/dynamic-plugin-sdk/src/extensions';
import { keywordCompare } from './catalog-utils';
import { CatalogFilter, CatalogFilterCounts, CatalogFilters } from './types';
import { sortCatalogItems } from './catalog-utils';
import { CatalogFilter, CatalogFilterCounts, CatalogFilters, CatalogSortOrder } from './types';

export const filterByGroup = (
items: CatalogItem[],
Expand Down Expand Up @@ -61,8 +61,9 @@ export const filterByAttributes = (
export const filterBySearchKeyword = (
items: CatalogItem[],
searchKeyword: string,
sortOrder: CatalogSortOrder = CatalogSortOrder.RELEVANCE,
): CatalogItem[] => {
return keywordCompare(searchKeyword, items);
return sortCatalogItems(items, sortOrder, searchKeyword);
};

export const filterByCategory = (
Expand Down
Loading