Skip to content

Commit

Permalink
Merge pull request #128 from fhlavac/sort
Browse files Browse the repository at this point in the history
  • Loading branch information
fhlavac authored Nov 21, 2024
2 parents 6d8dfcd + cefbeff commit 4cdda96
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 1 deletion.
109 changes: 109 additions & 0 deletions cypress/component/DataViewTableSorting.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-disable no-nested-ternary */
import React from 'react';
import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { ThProps } from '@patternfly/react-table';

interface Repository {
name: string;
branches: string;
prs: string;
workspaces: string;
lastCommit: string;
}

const COLUMNS = [
{ label: 'Repository', key: 'name', index: 0 },
{ label: 'Branch', key: 'branches', index: 1 },
{ label: 'Pull request', key: 'prs', index: 2 },
{ label: 'Workspace', key: 'workspaces', index: 3 },
{ label: 'Last commit', key: 'lastCommit', index: 4 },
];

const repositories: Repository[] = [
{ name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: '2023-11-01' },
{ name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: '2023-11-06' },
{ name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: '2023-11-02' },
{ name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: '2023-11-05' },
{ name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: '2023-11-03' },
{ name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: '2023-11-04' },
];

const sortData = (data: Repository[], sortBy: keyof Repository | undefined, direction: 'asc' | 'desc' | undefined) =>
sortBy && direction
? [ ...data ].sort((a, b) =>
direction === 'asc'
? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
: a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
)
: data;

const TestTable: React.FunctionComponent = () => {
const [ searchParams, setSearchParams ] = useSearchParams();
const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
const sortByIndex = React.useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);

const getSortParams = (columnIndex: number): ThProps['sort'] => ({
sortBy: {
index: sortByIndex,
direction,
defaultDirection: 'asc',
},
onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
columnIndex,
});

const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
cell: column.label,
props: { sort: getSortParams(index) },
}));

const rows: DataViewTr[] = React.useMemo(
() =>
sortData(repositories, sortBy ? sortBy as keyof Repository : undefined, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
name,
branches,
prs,
workspaces,
lastCommit,
]),
[ sortBy, direction ]
);

return <DataViewTable aria-label="Repositories table" ouiaId="test-table" columns={columns} rows={rows} />;
};

describe('DataViewTable Sorting with Hook', () => {
it('sorts by repository name in ascending and descending order', () => {
cy.mount(
<BrowserRouter>
<TestTable />
</BrowserRouter>
);

cy.get('[data-ouia-component-id="test-table-th-0"]').click();
cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository five');
cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository two');

cy.get('[data-ouia-component-id="test-table-th-0"]').click();
cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository two');
cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository five');
});

it('sorts by last commit date in ascending and descending order', () => {
cy.mount(
<BrowserRouter>
<TestTable />
</BrowserRouter>
);

cy.get('[data-ouia-component-id="test-table-th-4"]').click();
cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-01');
cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-06');

cy.get('[data-ouia-component-id="test-table-th-4"]').click();
cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-06');
cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-01');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/mod
---
import { useMemo } from 'react';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { useDataViewPagination, useDataViewSelection, useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
Expand Down Expand Up @@ -119,3 +119,34 @@ This example demonstrates the setup and usage of filters within the data view. I
```js file="./FiltersExample.tsx"

```

### Sort state

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.

**Initial values:**
- `initialSort` object to set default `sortBy` and `direction` values:
- `sortBy`: key of the initial column to sort.
- `direction`: default sorting direction (`asc` or `desc`).
- Optional `searchParams` object to manage URL-based synchronization of sort state.
- Optional `setSearchParams` function to update the URL parameters when sorting changes.
- `defaultDirection` to set the default direction when no direction is specified.
- Customizable parameter names for the URL:
- `sortByParam`: name of the URL parameter for the column key.
- `directionParam`: name of the URL parameter for the sorting direction.

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.

**Return values:**
- `sortBy`: key of the column currently being sorted.
- `direction`: current sorting direction (`asc` or `desc`).
- `onSort`: function to handle sorting changes programmatically or via user interaction.

### Sorting example

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.


```js file="./SortingExample.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable no-nested-ternary */
import React, { useMemo } from 'react';
import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { ThProps } from '@patternfly/react-table';
import { BrowserRouter, useSearchParams } from 'react-router-dom';

interface Repository {
name: string;
branches: string;
prs: string;
workspaces: string;
lastCommit: string;
};

const COLUMNS = [
{ label: 'Repository', key: 'name', index: 0 },
{ label: 'Branch', key: 'branches', index: 1 },
{ label: 'Pull request', key: 'prs', index: 2 },
{ label: 'Workspace', key: 'workspaces', index: 3 },
{ label: 'Last commit', key: 'lastCommit', index: 4 }
];

const repositories: Repository[] = [
{ name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
{ name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
{ name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
{ name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
{ name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
{ name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
];

const sortData = (data: Repository[], sortBy: string | undefined, direction: 'asc' | 'desc' | undefined) =>
sortBy && direction
? [ ...data ].sort((a, b) =>
direction === 'asc'
? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
: a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
)
: data;

const ouiaId = 'TableExample';

export const MyTable: React.FunctionComponent = () => {
const [ searchParams, setSearchParams ] = useSearchParams();
const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
const sortByIndex = useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);

const getSortParams = (columnIndex: number): ThProps['sort'] => ({
sortBy: {
index: sortByIndex,
direction,
defaultDirection: 'asc'
},
onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
columnIndex
});

const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
cell: column.label,
props: { sort: getSortParams(index) }
}));

const rows: DataViewTr[] = useMemo(() => sortData(repositories, sortBy, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
name,
branches,
prs,
workspaces,
lastCommit,
]), [ sortBy, direction ]);

return (
<DataViewTable
aria-label="Repositories table"
ouiaId={ouiaId}
columns={columns}
rows={rows}
/>
);
};

export const BasicExample: React.FunctionComponent = () => (
<BrowserRouter>
<MyTable/>
</BrowserRouter>
)

1 change: 1 addition & 0 deletions packages/module/src/Hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './pagination';
export * from './selection';
export * from './filters';
export * from './sort';
84 changes: 84 additions & 0 deletions packages/module/src/Hooks/sort.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import '@testing-library/jest-dom';
import { renderHook, act } from '@testing-library/react';
import { useDataViewSort, UseDataViewSortProps, DataViewSortConfig, DataViewSortParams } from './sort';

describe('useDataViewSort', () => {
const initialSort: DataViewSortConfig = { sortBy: 'name', direction: 'asc' };

it('should initialize with provided initial sort config', () => {
const { result } = renderHook(() => useDataViewSort({ initialSort }));
expect(result.current).toEqual(expect.objectContaining(initialSort));
});

it('should initialize with empty sort config if no initialSort is provided', () => {
const { result } = renderHook(() => useDataViewSort());
expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
});

it('should update sort state when onSort is called', () => {
const { result } = renderHook(() => useDataViewSort({ initialSort }));
act(() => {
result.current.onSort(undefined, 'age', 'desc');
});
expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
});

it('should sync with URL search params if isUrlSyncEnabled', () => {
const searchParams = new URLSearchParams();
const setSearchParams = jest.fn();
const props: UseDataViewSortProps = {
initialSort,
searchParams,
setSearchParams,
};

const { result } = renderHook(() => useDataViewSort(props));

expect(setSearchParams).toHaveBeenCalledTimes(1);
expect(result.current).toEqual(expect.objectContaining(initialSort));
});

it('should validate direction and fallback to default direction if invalid direction is provided', () => {
const searchParams = new URLSearchParams();
searchParams.set(DataViewSortParams.SORT_BY, 'name');
searchParams.set(DataViewSortParams.DIRECTION, 'invalid-direction');
const { result } = renderHook(() => useDataViewSort({ searchParams, defaultDirection: 'desc' }));

expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
});

it('should update search params when URL sync is enabled and sort changes', () => {
const searchParams = new URLSearchParams();
const setSearchParams = jest.fn();
const props: UseDataViewSortProps = {
initialSort,
searchParams,
setSearchParams,
};

const { result } = renderHook(() => useDataViewSort(props));
act(() => {
expect(setSearchParams).toHaveBeenCalledTimes(1);
result.current.onSort(undefined, 'priority', 'desc');
});

expect(setSearchParams).toHaveBeenCalledTimes(2);
expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
});

it('should prioritize searchParams values', () => {
const searchParams = new URLSearchParams();
searchParams.set(DataViewSortParams.SORT_BY, 'category');
searchParams.set(DataViewSortParams.DIRECTION, 'desc');

const { result } = renderHook(
(props: UseDataViewSortProps) => useDataViewSort(props),
{ initialProps: { initialSort, searchParams } }
);

expect(result.current).toEqual(expect.objectContaining({
sortBy: 'category',
direction: 'desc',
}));
});
});
Loading

0 comments on commit 4cdda96

Please sign in to comment.