Skip to content

Commit 4cdda96

Browse files
authored
Merge pull request #128 from fhlavac/sort
2 parents 6d8dfcd + cefbeff commit 4cdda96

File tree

6 files changed

+400
-1
lines changed

6 files changed

+400
-1
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint-disable no-nested-ternary */
2+
import React 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 { BrowserRouter, useSearchParams } from 'react-router-dom';
6+
import { ThProps } from '@patternfly/react-table';
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: '2023-11-01' },
26+
{ name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: '2023-11-06' },
27+
{ name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: '2023-11-02' },
28+
{ name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: '2023-11-05' },
29+
{ name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: '2023-11-03' },
30+
{ name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: '2023-11-04' },
31+
];
32+
33+
const sortData = (data: Repository[], sortBy: keyof Repository | 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 TestTable: React.FunctionComponent = () => {
43+
const [ searchParams, setSearchParams ] = useSearchParams();
44+
const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
45+
const sortByIndex = React.useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);
46+
47+
const getSortParams = (columnIndex: number): ThProps['sort'] => ({
48+
sortBy: {
49+
index: sortByIndex,
50+
direction,
51+
defaultDirection: 'asc',
52+
},
53+
onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
54+
columnIndex,
55+
});
56+
57+
const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
58+
cell: column.label,
59+
props: { sort: getSortParams(index) },
60+
}));
61+
62+
const rows: DataViewTr[] = React.useMemo(
63+
() =>
64+
sortData(repositories, sortBy ? sortBy as keyof Repository : undefined, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
65+
name,
66+
branches,
67+
prs,
68+
workspaces,
69+
lastCommit,
70+
]),
71+
[ sortBy, direction ]
72+
);
73+
74+
return <DataViewTable aria-label="Repositories table" ouiaId="test-table" columns={columns} rows={rows} />;
75+
};
76+
77+
describe('DataViewTable Sorting with Hook', () => {
78+
it('sorts by repository name in ascending and descending order', () => {
79+
cy.mount(
80+
<BrowserRouter>
81+
<TestTable />
82+
</BrowserRouter>
83+
);
84+
85+
cy.get('[data-ouia-component-id="test-table-th-0"]').click();
86+
cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository five');
87+
cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository two');
88+
89+
cy.get('[data-ouia-component-id="test-table-th-0"]').click();
90+
cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository two');
91+
cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository five');
92+
});
93+
94+
it('sorts by last commit date in ascending and descending order', () => {
95+
cy.mount(
96+
<BrowserRouter>
97+
<TestTable />
98+
</BrowserRouter>
99+
);
100+
101+
cy.get('[data-ouia-component-id="test-table-th-4"]').click();
102+
cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-01');
103+
cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-06');
104+
105+
cy.get('[data-ouia-component-id="test-table-th-4"]').click();
106+
cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-06');
107+
cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-01');
108+
});
109+
});

packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/mod
1616
---
1717
import { useMemo } from 'react';
1818
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';
2020
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
2121
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
2222
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
119119
```js file="./FiltersExample.tsx"
120120

121121
```
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+
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

packages/module/src/Hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './pagination';
22
export * from './selection';
33
export * from './filters';
4+
export * from './sort';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)