@patternfly/react-data-view 5.6.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
1
  export * from './pagination';
2
2
  export * from './selection';
3
3
  export * from './filters';
4
+ export * from './sort';
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./pagination"), exports);
18
18
  __exportStar(require("./selection"), exports);
19
19
  __exportStar(require("./filters"), exports);
20
+ __exportStar(require("./sort"), exports);
@@ -0,0 +1,32 @@
1
+ import { ISortBy } from "@patternfly/react-table";
2
+ export declare enum DataViewSortParams {
3
+ SORT_BY = "sortBy",
4
+ DIRECTION = "direction"
5
+ }
6
+ export interface DataViewSortConfig {
7
+ /** Attribute to sort the entries by */
8
+ sortBy: string | undefined;
9
+ /** Sort direction */
10
+ direction: ISortBy['direction'];
11
+ }
12
+ export interface UseDataViewSortProps {
13
+ /** Initial sort config */
14
+ initialSort?: DataViewSortConfig;
15
+ /** Current search parameters as a string */
16
+ searchParams?: URLSearchParams;
17
+ /** Function to set search parameters */
18
+ setSearchParams?: (params: URLSearchParams) => void;
19
+ /** Default direction */
20
+ defaultDirection?: ISortBy['direction'];
21
+ /** Sort by URL param name */
22
+ sortByParam?: string;
23
+ /** Direction URL param name */
24
+ directionParam?: string;
25
+ }
26
+ export declare const useDataViewSort: (props?: UseDataViewSortProps) => {
27
+ onSort: (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined, newSortBy: string, newSortDirection: ISortBy['direction']) => void;
28
+ /** Attribute to sort the entries by */
29
+ sortBy: string | undefined;
30
+ /** Sort direction */
31
+ direction: ISortBy['direction'];
32
+ };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useDataViewSort = exports.DataViewSortParams = void 0;
4
+ const react_1 = require("react");
5
+ var DataViewSortParams;
6
+ (function (DataViewSortParams) {
7
+ DataViewSortParams["SORT_BY"] = "sortBy";
8
+ DataViewSortParams["DIRECTION"] = "direction";
9
+ })(DataViewSortParams || (exports.DataViewSortParams = DataViewSortParams = {}));
10
+ ;
11
+ const validateDirection = (direction, defaultDirection) => (direction === 'asc' || direction === 'desc' ? direction : defaultDirection);
12
+ ;
13
+ ;
14
+ const useDataViewSort = (props) => {
15
+ var _a;
16
+ const { initialSort, searchParams, setSearchParams, defaultDirection = 'asc', sortByParam = DataViewSortParams.SORT_BY, directionParam = DataViewSortParams.DIRECTION } = props !== null && props !== void 0 ? props : {};
17
+ const isUrlSyncEnabled = (0, react_1.useMemo)(() => searchParams && !!setSearchParams, [searchParams, setSearchParams]);
18
+ const [state, setState] = (0, react_1.useState)({
19
+ sortBy: (_a = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(sortByParam)) !== null && _a !== void 0 ? _a : initialSort === null || initialSort === void 0 ? void 0 : initialSort.sortBy,
20
+ direction: validateDirection(searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(directionParam), initialSort === null || initialSort === void 0 ? void 0 : initialSort.direction),
21
+ });
22
+ const updateSearchParams = (sortBy, direction) => {
23
+ if (isUrlSyncEnabled && sortBy) {
24
+ const params = new URLSearchParams(searchParams);
25
+ params.set(sortByParam, `${sortBy}`);
26
+ params.set(directionParam, `${direction}`);
27
+ setSearchParams === null || setSearchParams === void 0 ? void 0 : setSearchParams(params);
28
+ }
29
+ };
30
+ (0, react_1.useEffect)(() => {
31
+ state.sortBy && state.direction && updateSearchParams(state.sortBy, state.direction);
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, []);
34
+ (0, react_1.useEffect)(() => {
35
+ const currentSortBy = (searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(sortByParam)) || state.sortBy;
36
+ const currentDirection = (searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(directionParam)) || state.direction;
37
+ const validDirection = validateDirection(currentDirection, defaultDirection);
38
+ currentSortBy !== state.sortBy || validDirection !== state.direction && setState({ sortBy: currentSortBy, direction: validDirection });
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [searchParams === null || searchParams === void 0 ? void 0 : searchParams.toString()]);
41
+ const onSort = (_event, newSortBy, newSortDirection) => {
42
+ setState({ sortBy: newSortBy, direction: newSortDirection });
43
+ updateSearchParams(newSortBy, newSortDirection);
44
+ };
45
+ return Object.assign(Object.assign({}, state), { onSort });
46
+ };
47
+ exports.useDataViewSort = useDataViewSort;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("@testing-library/jest-dom");
4
+ const react_1 = require("@testing-library/react");
5
+ const sort_1 = require("./sort");
6
+ describe('useDataViewSort', () => {
7
+ const initialSort = { sortBy: 'name', direction: 'asc' };
8
+ it('should initialize with provided initial sort config', () => {
9
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)({ initialSort }));
10
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
11
+ });
12
+ it('should initialize with empty sort config if no initialSort is provided', () => {
13
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)());
14
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
15
+ });
16
+ it('should update sort state when onSort is called', () => {
17
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)({ initialSort }));
18
+ (0, react_1.act)(() => {
19
+ result.current.onSort(undefined, 'age', 'desc');
20
+ });
21
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
22
+ });
23
+ it('should sync with URL search params if isUrlSyncEnabled', () => {
24
+ const searchParams = new URLSearchParams();
25
+ const setSearchParams = jest.fn();
26
+ const props = {
27
+ initialSort,
28
+ searchParams,
29
+ setSearchParams,
30
+ };
31
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)(props));
32
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
33
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
34
+ });
35
+ it('should validate direction and fallback to default direction if invalid direction is provided', () => {
36
+ const searchParams = new URLSearchParams();
37
+ searchParams.set(sort_1.DataViewSortParams.SORT_BY, 'name');
38
+ searchParams.set(sort_1.DataViewSortParams.DIRECTION, 'invalid-direction');
39
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)({ searchParams, defaultDirection: 'desc' }));
40
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
41
+ });
42
+ it('should update search params when URL sync is enabled and sort changes', () => {
43
+ const searchParams = new URLSearchParams();
44
+ const setSearchParams = jest.fn();
45
+ const props = {
46
+ initialSort,
47
+ searchParams,
48
+ setSearchParams,
49
+ };
50
+ const { result } = (0, react_1.renderHook)(() => (0, sort_1.useDataViewSort)(props));
51
+ (0, react_1.act)(() => {
52
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
53
+ result.current.onSort(undefined, 'priority', 'desc');
54
+ });
55
+ expect(setSearchParams).toHaveBeenCalledTimes(2);
56
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
57
+ });
58
+ it('should prioritize searchParams values', () => {
59
+ const searchParams = new URLSearchParams();
60
+ searchParams.set(sort_1.DataViewSortParams.SORT_BY, 'category');
61
+ searchParams.set(sort_1.DataViewSortParams.DIRECTION, 'desc');
62
+ const { result } = (0, react_1.renderHook)((props) => (0, sort_1.useDataViewSort)(props), { initialProps: { initialSort, searchParams } });
63
+ expect(result.current).toEqual(expect.objectContaining({
64
+ sortBy: 'category',
65
+ direction: 'desc',
66
+ }));
67
+ });
68
+ });
@@ -1,3 +1,4 @@
1
1
  export * from './pagination';
2
2
  export * from './selection';
3
3
  export * from './filters';
4
+ export * from './sort';
@@ -1,3 +1,4 @@
1
1
  export * from './pagination';
2
2
  export * from './selection';
3
3
  export * from './filters';
4
+ export * from './sort';
@@ -0,0 +1,32 @@
1
+ import { ISortBy } from "@patternfly/react-table";
2
+ export declare enum DataViewSortParams {
3
+ SORT_BY = "sortBy",
4
+ DIRECTION = "direction"
5
+ }
6
+ export interface DataViewSortConfig {
7
+ /** Attribute to sort the entries by */
8
+ sortBy: string | undefined;
9
+ /** Sort direction */
10
+ direction: ISortBy['direction'];
11
+ }
12
+ export interface UseDataViewSortProps {
13
+ /** Initial sort config */
14
+ initialSort?: DataViewSortConfig;
15
+ /** Current search parameters as a string */
16
+ searchParams?: URLSearchParams;
17
+ /** Function to set search parameters */
18
+ setSearchParams?: (params: URLSearchParams) => void;
19
+ /** Default direction */
20
+ defaultDirection?: ISortBy['direction'];
21
+ /** Sort by URL param name */
22
+ sortByParam?: string;
23
+ /** Direction URL param name */
24
+ directionParam?: string;
25
+ }
26
+ export declare const useDataViewSort: (props?: UseDataViewSortProps) => {
27
+ onSort: (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined, newSortBy: string, newSortDirection: ISortBy['direction']) => void;
28
+ /** Attribute to sort the entries by */
29
+ sortBy: string | undefined;
30
+ /** Sort direction */
31
+ direction: ISortBy['direction'];
32
+ };
@@ -0,0 +1,43 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ export var DataViewSortParams;
3
+ (function (DataViewSortParams) {
4
+ DataViewSortParams["SORT_BY"] = "sortBy";
5
+ DataViewSortParams["DIRECTION"] = "direction";
6
+ })(DataViewSortParams || (DataViewSortParams = {}));
7
+ ;
8
+ const validateDirection = (direction, defaultDirection) => (direction === 'asc' || direction === 'desc' ? direction : defaultDirection);
9
+ ;
10
+ ;
11
+ export const useDataViewSort = (props) => {
12
+ var _a;
13
+ const { initialSort, searchParams, setSearchParams, defaultDirection = 'asc', sortByParam = DataViewSortParams.SORT_BY, directionParam = DataViewSortParams.DIRECTION } = props !== null && props !== void 0 ? props : {};
14
+ const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [searchParams, setSearchParams]);
15
+ const [state, setState] = useState({
16
+ sortBy: (_a = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(sortByParam)) !== null && _a !== void 0 ? _a : initialSort === null || initialSort === void 0 ? void 0 : initialSort.sortBy,
17
+ direction: validateDirection(searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(directionParam), initialSort === null || initialSort === void 0 ? void 0 : initialSort.direction),
18
+ });
19
+ const updateSearchParams = (sortBy, direction) => {
20
+ if (isUrlSyncEnabled && sortBy) {
21
+ const params = new URLSearchParams(searchParams);
22
+ params.set(sortByParam, `${sortBy}`);
23
+ params.set(directionParam, `${direction}`);
24
+ setSearchParams === null || setSearchParams === void 0 ? void 0 : setSearchParams(params);
25
+ }
26
+ };
27
+ useEffect(() => {
28
+ state.sortBy && state.direction && updateSearchParams(state.sortBy, state.direction);
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, []);
31
+ useEffect(() => {
32
+ const currentSortBy = (searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(sortByParam)) || state.sortBy;
33
+ const currentDirection = (searchParams === null || searchParams === void 0 ? void 0 : searchParams.get(directionParam)) || state.direction;
34
+ const validDirection = validateDirection(currentDirection, defaultDirection);
35
+ currentSortBy !== state.sortBy || validDirection !== state.direction && setState({ sortBy: currentSortBy, direction: validDirection });
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [searchParams === null || searchParams === void 0 ? void 0 : searchParams.toString()]);
38
+ const onSort = (_event, newSortBy, newSortDirection) => {
39
+ setState({ sortBy: newSortBy, direction: newSortDirection });
40
+ updateSearchParams(newSortBy, newSortDirection);
41
+ };
42
+ return Object.assign(Object.assign({}, state), { onSort });
43
+ };
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,66 @@
1
+ import '@testing-library/jest-dom';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useDataViewSort, DataViewSortParams } from './sort';
4
+ describe('useDataViewSort', () => {
5
+ const initialSort = { sortBy: 'name', direction: 'asc' };
6
+ it('should initialize with provided initial sort config', () => {
7
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
8
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
9
+ });
10
+ it('should initialize with empty sort config if no initialSort is provided', () => {
11
+ const { result } = renderHook(() => useDataViewSort());
12
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
13
+ });
14
+ it('should update sort state when onSort is called', () => {
15
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
16
+ act(() => {
17
+ result.current.onSort(undefined, 'age', 'desc');
18
+ });
19
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
20
+ });
21
+ it('should sync with URL search params if isUrlSyncEnabled', () => {
22
+ const searchParams = new URLSearchParams();
23
+ const setSearchParams = jest.fn();
24
+ const props = {
25
+ initialSort,
26
+ searchParams,
27
+ setSearchParams,
28
+ };
29
+ const { result } = renderHook(() => useDataViewSort(props));
30
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
31
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
32
+ });
33
+ it('should validate direction and fallback to default direction if invalid direction is provided', () => {
34
+ const searchParams = new URLSearchParams();
35
+ searchParams.set(DataViewSortParams.SORT_BY, 'name');
36
+ searchParams.set(DataViewSortParams.DIRECTION, 'invalid-direction');
37
+ const { result } = renderHook(() => useDataViewSort({ searchParams, defaultDirection: 'desc' }));
38
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
39
+ });
40
+ it('should update search params when URL sync is enabled and sort changes', () => {
41
+ const searchParams = new URLSearchParams();
42
+ const setSearchParams = jest.fn();
43
+ const props = {
44
+ initialSort,
45
+ searchParams,
46
+ setSearchParams,
47
+ };
48
+ const { result } = renderHook(() => useDataViewSort(props));
49
+ act(() => {
50
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
51
+ result.current.onSort(undefined, 'priority', 'desc');
52
+ });
53
+ expect(setSearchParams).toHaveBeenCalledTimes(2);
54
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
55
+ });
56
+ it('should prioritize searchParams values', () => {
57
+ const searchParams = new URLSearchParams();
58
+ searchParams.set(DataViewSortParams.SORT_BY, 'category');
59
+ searchParams.set(DataViewSortParams.DIRECTION, 'desc');
60
+ const { result } = renderHook((props) => useDataViewSort(props), { initialProps: { initialSort, searchParams } });
61
+ expect(result.current).toEqual(expect.objectContaining({
62
+ sortBy: 'category',
63
+ direction: 'desc',
64
+ }));
65
+ });
66
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-data-view",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "description": "Data view used for Red Hat projects.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -16,7 +16,7 @@ sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/mod
16
16
  ---
17
17
  import { useMemo } from 'react';
18
18
  import { BrowserRouter, useSearchParams } from 'react-router-dom';
19
- import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
19
+ import { useDataViewPagination, useDataViewSelection, useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
20
20
  import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
21
21
  import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
22
22
  import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
@@ -119,3 +119,34 @@ This example demonstrates the setup and usage of filters within the data view. I
119
119
  ```js file="./FiltersExample.tsx"
120
120
 
121
121
  ```
122
+
123
+ ### Sort state
124
+
125
+ The `useDataViewSort` hook manages the sorting state of a data view. It provides an easy way to handle sorting logic, including synchronization with URL parameters and defining default sorting behavior.
126
+
127
+ **Initial values:**
128
+ - `initialSort` object to set default `sortBy` and `direction` values:
129
+ - `sortBy`: key of the initial column to sort.
130
+ - `direction`: default sorting direction (`asc` or `desc`).
131
+ - Optional `searchParams` object to manage URL-based synchronization of sort state.
132
+ - Optional `setSearchParams` function to update the URL parameters when sorting changes.
133
+ - `defaultDirection` to set the default direction when no direction is specified.
134
+ - Customizable parameter names for the URL:
135
+ - `sortByParam`: name of the URL parameter for the column key.
136
+ - `directionParam`: name of the URL parameter for the sorting direction.
137
+
138
+ The `useDataViewSort` hook integrates seamlessly with React Router to manage sort state via URL parameters. Alternatively, you can use `URLSearchParams` and `window.history.pushState` APIs, or other routing libraries. If URL synchronization is not configured, the sort state is managed internally within the component.
139
+
140
+ **Return values:**
141
+ - `sortBy`: key of the column currently being sorted.
142
+ - `direction`: current sorting direction (`asc` or `desc`).
143
+ - `onSort`: function to handle sorting changes programmatically or via user interaction.
144
+
145
+ ### Sorting example
146
+
147
+ This example demonstrates how to set up and use sorting functionality within a data view. The implementation includes dynamic sorting by column with persistence of sort state in the URL using React Router.
148
+
149
+
150
+ ```js file="./SortingExample.tsx"
151
+
152
+ ```
@@ -0,0 +1,87 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import React, { useMemo } from 'react';
3
+ import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
4
+ import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
5
+ import { ThProps } from '@patternfly/react-table';
6
+ import { BrowserRouter, useSearchParams } from 'react-router-dom';
7
+
8
+ interface Repository {
9
+ name: string;
10
+ branches: string;
11
+ prs: string;
12
+ workspaces: string;
13
+ lastCommit: string;
14
+ };
15
+
16
+ const COLUMNS = [
17
+ { label: 'Repository', key: 'name', index: 0 },
18
+ { label: 'Branch', key: 'branches', index: 1 },
19
+ { label: 'Pull request', key: 'prs', index: 2 },
20
+ { label: 'Workspace', key: 'workspaces', index: 3 },
21
+ { label: 'Last commit', key: 'lastCommit', index: 4 }
22
+ ];
23
+
24
+ const repositories: Repository[] = [
25
+ { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
26
+ { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
27
+ { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
28
+ { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
29
+ { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
30
+ { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
31
+ ];
32
+
33
+ const sortData = (data: Repository[], sortBy: string | undefined, direction: 'asc' | 'desc' | undefined) =>
34
+ sortBy && direction
35
+ ? [ ...data ].sort((a, b) =>
36
+ direction === 'asc'
37
+ ? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
38
+ : a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
39
+ )
40
+ : data;
41
+
42
+ const ouiaId = 'TableExample';
43
+
44
+ export const MyTable: React.FunctionComponent = () => {
45
+ const [ searchParams, setSearchParams ] = useSearchParams();
46
+ const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
47
+ const sortByIndex = useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);
48
+
49
+ const getSortParams = (columnIndex: number): ThProps['sort'] => ({
50
+ sortBy: {
51
+ index: sortByIndex,
52
+ direction,
53
+ defaultDirection: 'asc'
54
+ },
55
+ onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
56
+ columnIndex
57
+ });
58
+
59
+ const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
60
+ cell: column.label,
61
+ props: { sort: getSortParams(index) }
62
+ }));
63
+
64
+ const rows: DataViewTr[] = useMemo(() => sortData(repositories, sortBy, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
65
+ name,
66
+ branches,
67
+ prs,
68
+ workspaces,
69
+ lastCommit,
70
+ ]), [ sortBy, direction ]);
71
+
72
+ return (
73
+ <DataViewTable
74
+ aria-label="Repositories table"
75
+ ouiaId={ouiaId}
76
+ columns={columns}
77
+ rows={rows}
78
+ />
79
+ );
80
+ };
81
+
82
+ export const BasicExample: React.FunctionComponent = () => (
83
+ <BrowserRouter>
84
+ <MyTable/>
85
+ </BrowserRouter>
86
+ )
87
+
@@ -1,3 +1,4 @@
1
1
  export * from './pagination';
2
2
  export * from './selection';
3
3
  export * from './filters';
4
+ export * from './sort';
@@ -0,0 +1,84 @@
1
+ import '@testing-library/jest-dom';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useDataViewSort, UseDataViewSortProps, DataViewSortConfig, DataViewSortParams } from './sort';
4
+
5
+ describe('useDataViewSort', () => {
6
+ const initialSort: DataViewSortConfig = { sortBy: 'name', direction: 'asc' };
7
+
8
+ it('should initialize with provided initial sort config', () => {
9
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
10
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
11
+ });
12
+
13
+ it('should initialize with empty sort config if no initialSort is provided', () => {
14
+ const { result } = renderHook(() => useDataViewSort());
15
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
16
+ });
17
+
18
+ it('should update sort state when onSort is called', () => {
19
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
20
+ act(() => {
21
+ result.current.onSort(undefined, 'age', 'desc');
22
+ });
23
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
24
+ });
25
+
26
+ it('should sync with URL search params if isUrlSyncEnabled', () => {
27
+ const searchParams = new URLSearchParams();
28
+ const setSearchParams = jest.fn();
29
+ const props: UseDataViewSortProps = {
30
+ initialSort,
31
+ searchParams,
32
+ setSearchParams,
33
+ };
34
+
35
+ const { result } = renderHook(() => useDataViewSort(props));
36
+
37
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
38
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
39
+ });
40
+
41
+ it('should validate direction and fallback to default direction if invalid direction is provided', () => {
42
+ const searchParams = new URLSearchParams();
43
+ searchParams.set(DataViewSortParams.SORT_BY, 'name');
44
+ searchParams.set(DataViewSortParams.DIRECTION, 'invalid-direction');
45
+ const { result } = renderHook(() => useDataViewSort({ searchParams, defaultDirection: 'desc' }));
46
+
47
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
48
+ });
49
+
50
+ it('should update search params when URL sync is enabled and sort changes', () => {
51
+ const searchParams = new URLSearchParams();
52
+ const setSearchParams = jest.fn();
53
+ const props: UseDataViewSortProps = {
54
+ initialSort,
55
+ searchParams,
56
+ setSearchParams,
57
+ };
58
+
59
+ const { result } = renderHook(() => useDataViewSort(props));
60
+ act(() => {
61
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
62
+ result.current.onSort(undefined, 'priority', 'desc');
63
+ });
64
+
65
+ expect(setSearchParams).toHaveBeenCalledTimes(2);
66
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
67
+ });
68
+
69
+ it('should prioritize searchParams values', () => {
70
+ const searchParams = new URLSearchParams();
71
+ searchParams.set(DataViewSortParams.SORT_BY, 'category');
72
+ searchParams.set(DataViewSortParams.DIRECTION, 'desc');
73
+
74
+ const { result } = renderHook(
75
+ (props: UseDataViewSortProps) => useDataViewSort(props),
76
+ { initialProps: { initialSort, searchParams } }
77
+ );
78
+
79
+ expect(result.current).toEqual(expect.objectContaining({
80
+ sortBy: 'category',
81
+ direction: 'desc',
82
+ }));
83
+ });
84
+ });
@@ -0,0 +1,87 @@
1
+ import { ISortBy } from "@patternfly/react-table";
2
+ import { useState, useEffect, useMemo } from "react";
3
+
4
+ export enum DataViewSortParams {
5
+ SORT_BY = 'sortBy',
6
+ DIRECTION = 'direction'
7
+ };
8
+
9
+ const validateDirection = (direction: string | null | undefined, defaultDirection: ISortBy['direction']): ISortBy['direction'] => (
10
+ direction === 'asc' || direction === 'desc' ? direction : defaultDirection
11
+ );
12
+
13
+ export interface DataViewSortConfig {
14
+ /** Attribute to sort the entries by */
15
+ sortBy: string | undefined;
16
+ /** Sort direction */
17
+ direction: ISortBy['direction'];
18
+ };
19
+
20
+ export interface UseDataViewSortProps {
21
+ /** Initial sort config */
22
+ initialSort?: DataViewSortConfig;
23
+ /** Current search parameters as a string */
24
+ searchParams?: URLSearchParams;
25
+ /** Function to set search parameters */
26
+ setSearchParams?: (params: URLSearchParams) => void;
27
+ /** Default direction */
28
+ defaultDirection?: ISortBy['direction'];
29
+ /** Sort by URL param name */
30
+ sortByParam?: string;
31
+ /** Direction URL param name */
32
+ directionParam?: string;
33
+ };
34
+
35
+ export const useDataViewSort = (props?: UseDataViewSortProps) => {
36
+ const {
37
+ initialSort,
38
+ searchParams,
39
+ setSearchParams,
40
+ defaultDirection = 'asc',
41
+ sortByParam = DataViewSortParams.SORT_BY,
42
+ directionParam = DataViewSortParams.DIRECTION
43
+ } = props ?? {};
44
+
45
+ const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [ searchParams, setSearchParams ]);
46
+
47
+ const [ state, setState ] = useState<DataViewSortConfig>({
48
+ sortBy: searchParams?.get(sortByParam) ?? initialSort?.sortBy,
49
+ direction: validateDirection(searchParams?.get(directionParam) as ISortBy['direction'], initialSort?.direction),
50
+ });
51
+
52
+ const updateSearchParams = (sortBy: string, direction: ISortBy['direction']) => {
53
+ if (isUrlSyncEnabled && sortBy) {
54
+ const params = new URLSearchParams(searchParams);
55
+ params.set(sortByParam, `${sortBy}`);
56
+ params.set(directionParam, `${direction}`);
57
+ setSearchParams?.(params);
58
+ }
59
+ };
60
+
61
+ useEffect(() => {
62
+ state.sortBy && state.direction && updateSearchParams(state.sortBy, state.direction);
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, []);
65
+
66
+ useEffect(() => {
67
+ const currentSortBy = searchParams?.get(sortByParam) || state.sortBy;
68
+ const currentDirection = searchParams?.get(directionParam) as ISortBy['direction'] || state.direction;
69
+ const validDirection = validateDirection(currentDirection, defaultDirection);
70
+ currentSortBy !== state.sortBy || validDirection !== state.direction && setState({ sortBy: currentSortBy, direction: validDirection });
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, [ searchParams?.toString() ]);
73
+
74
+ const onSort = (
75
+ _event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined,
76
+ newSortBy: string,
77
+ newSortDirection: ISortBy['direction']
78
+ ) => {
79
+ setState({ sortBy: newSortBy, direction: newSortDirection });
80
+ updateSearchParams(newSortBy, newSortDirection);
81
+ };
82
+
83
+ return {
84
+ ...state,
85
+ onSort
86
+ };
87
+ };