diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..2f6e5bc --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,9 @@ +export interface Page { + page: number; + perPage: number; +} + +export interface SortBy { + index: number; + direction: 'asc' | 'desc'; +} diff --git a/src/hooks/useTable/index.ts b/src/hooks/useTable/index.ts new file mode 100644 index 0000000..0ad8d00 --- /dev/null +++ b/src/hooks/useTable/index.ts @@ -0,0 +1 @@ +export { useTable } from './useTable'; diff --git a/src/hooks/useTable/useTable.test.tsx b/src/hooks/useTable/useTable.test.tsx new file mode 100644 index 0000000..166d5d6 --- /dev/null +++ b/src/hooks/useTable/useTable.test.tsx @@ -0,0 +1,144 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { Page } from '../../common/types'; +import { useTable } from './useTable'; + +describe('useTableFilter', () => { + it('Pagination', () => { + const items = [...Array(15)].map((_, index) => index + 1); + const page: Page = { page: 1, perPage: 10 }; + + const { result } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: () => true, + compareToByColumn: () => 1, + }) + ); + + // Page1 + expect(result.current.pageItems).toEqual(items.slice(0, 10)); + }); + + it('Filter', () => { + const items = [...Array(15)].map((_, index) => index + 1); + const page: Page = { page: 1, perPage: 10 }; + + const { result } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: (value) => value % 2 === 1, + compareToByColumn: () => 1, + }) + ); + + // Page1 + const expectedResult = [1, 3, 5, 7, 9, 11, 13, 15]; + expect(result.current.pageItems).toEqual(expectedResult); + }); + + it('SortBy', () => { + const items = [...Array(15)].map((_, index) => index + 1); + const page: Page = { page: 1, perPage: 10 }; + const filterItem = () => true; + const compareToByColumn = (a: number, b: number, indexCol?: number) => + indexCol === 7 ? a - b : 0; + + // Verify asc + const { result: resultAsc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: filterItem, + currentSortBy: { direction: 'asc', index: 7 }, + compareToByColumn: compareToByColumn, + }) + ); + + const expectedAscResult = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + expect(resultAsc.current.pageItems).toEqual(expectedAscResult); + + // Verify desc + const { result: resultDesc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: filterItem, + currentSortBy: { direction: 'desc', index: 7 }, + compareToByColumn: compareToByColumn, + }) + ); + + const expectedDescResult = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6]; + expect(resultDesc.current.pageItems).toEqual(expectedDescResult); + }); + + it("SortBy when 'compareToByColumn' return always 0", () => { + const items = [...Array(15)].map((_, index) => index + 1); + const page: Page = { page: 1, perPage: 10 }; + + const expectedResult = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + // Verify asc + const { result: resultAsc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: () => true, + currentSortBy: { direction: 'asc', index: 7 }, + compareToByColumn: () => 0, // forcing comparison true + }) + ); + + expect(resultAsc.current.pageItems).toEqual(expectedResult); + + // Verify desc + const { result: resultDesc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: () => true, + currentSortBy: { direction: 'desc', index: 7 }, + compareToByColumn: () => 0, // forcing comparison true + }) + ); + + expect(resultDesc.current.pageItems).toEqual(expectedResult); + }); + + it("SortBy when 'compareToByColumn' return always 0 and 'filter' is applied", () => { + const items = [...Array(25)].map((_, index) => index + 1); + const page: Page = { page: 1, perPage: 10 }; + const filterItem = (val: number) => val % 2 === 0; + const compareToByColumn = () => 0; // forcing comparison true + + const expectedResult = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]; + + // Verify asc + const { result: resultAsc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: filterItem, + currentSortBy: { direction: 'asc', index: 7 }, + compareToByColumn: compareToByColumn, + }) + ); + + expect(resultAsc.current.pageItems).toEqual(expectedResult); + + // Verify desc + const { result: resultDesc } = renderHook(() => + useTable({ + items: items, + currentPage: page, + filterItem: filterItem, + currentSortBy: { direction: 'desc', index: 7 }, + compareToByColumn: compareToByColumn, + }) + ); + + expect(resultDesc.current.pageItems).toEqual(expectedResult); + }); +}); diff --git a/src/hooks/useTable/useTable.ts b/src/hooks/useTable/useTable.ts new file mode 100644 index 0000000..764ff11 --- /dev/null +++ b/src/hooks/useTable/useTable.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react'; +import { SortByDirection } from '@patternfly/react-table'; +import { Page, SortBy } from '../../common/types'; + +// Hook + +interface HookArgs { + items?: T[]; + + currentSortBy?: SortBy; + compareToByColumn: (a: T, b: T, columnIndex?: number) => number; + + currentPage: Page; + filterItem: (value: T) => boolean; +} + +interface HookState { + pageItems: T[]; + filteredItems: T[]; +} + +export const useTable = ({ + items, + currentSortBy, + currentPage, + filterItem, + compareToByColumn, +}: HookArgs): HookState => { + const state: HookState = useMemo(() => { + const allItems = [...(items || [])]; + + // Filter + const filteredItems = allItems.filter(filterItem); + + // Sort + let orderChanged = false; + + let sortedItems: T[]; + sortedItems = [...filteredItems].sort((a, b) => { + const comparisonResult = compareToByColumn(a, b, currentSortBy?.index); + if (comparisonResult !== 0) { + orderChanged = true; + } + return comparisonResult; + }); + + if (orderChanged && currentSortBy?.direction === SortByDirection.desc) { + sortedItems = sortedItems.reverse(); + } + + // Paginate + const pageItems = sortedItems.slice( + (currentPage.page - 1) * currentPage.perPage, + currentPage.page * currentPage.perPage + ); + + return { + pageItems, + filteredItems, + }; + }, [items, currentPage, currentSortBy, compareToByColumn, filterItem]); + + return state; +}; diff --git a/src/hooks/useTableControls/useTableControls.ts b/src/hooks/useTableControls/useTableControls.ts index dd3557b..fea096e 100644 --- a/src/hooks/useTableControls/useTableControls.ts +++ b/src/hooks/useTableControls/useTableControls.ts @@ -1,16 +1,7 @@ import { useCallback, useReducer } from 'react'; import { ActionType, createAction, getType } from 'typesafe-actions'; import { IExtraColumnData, SortByDirection } from '@patternfly/react-table'; - -export interface Page { - page: number; - perPage: number; -} - -export interface SortBy { - index: number; - direction: 'asc' | 'desc'; -} +import { Page, SortBy } from '../../common/types'; // Actions diff --git a/src/index.ts b/src/index.ts index bd89288..2c2a8f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ export * from './components/StatusIcon'; export * from './hooks/useDelete'; export * from './hooks/useModal'; +export * from './hooks/useTable'; export * from './hooks/useTableControls'; export * from './hooks/useToolbar';