Skip to content

Commit ab3fc5a

Browse files
committed
feat: refactor Column into ColumnManagement and add patternfly-docs/examples
1 parent 1c1a4ed commit ab3fc5a

File tree

8 files changed

+183
-121
lines changed

8 files changed

+183
-121
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
# Sidenav top-level section
3+
# should be the same for all markdown files
4+
section: Component groups
5+
subsection: Helpers
6+
# Sidenav secondary level section
7+
# should be the same for all markdown files
8+
id: Column management
9+
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
10+
source: react
11+
# If you use typescript, the name of the interface to display props for
12+
# These are found through the sourceProps function provided in patternfly-docs.source.js
13+
propComponents: ['ColumnManagement']
14+
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md
15+
---
16+
17+
import ColumnManagement from '@patternfly/react-component-groups/dist/dynamic/ColumnManagement';
18+
import { FunctionComponent, useState } from 'react';
19+
20+
The **column management** component can be used to implement customizable table columns. Columns can be configured to be enabled or disabled by default or be unhidable.
21+
22+
## Examples
23+
24+
### Basic column list
25+
26+
The order of the columns can be changed by dragging and dropping the columns themselves. This list can be used within a page or within a modal. Always make sure to set `isShownByDefault` and `isShown` to the same boolean value in the initial state.
27+
28+
```js file="./ColumnManagementExample.tsx"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { FunctionComponent, useState } from 'react';
2+
import { Column, ColumnManagement } from '@patternfly/react-component-groups';
3+
4+
const DEFAULT_COLUMNS: Column[] = [
5+
{
6+
title: 'ID',
7+
key: 'id',
8+
isShownByDefault: true,
9+
isShown: true,
10+
isUntoggleable: true
11+
},
12+
{
13+
title: 'Publish date',
14+
key: 'publishDate',
15+
isShownByDefault: true,
16+
isShown: true
17+
},
18+
{
19+
title: 'Impact',
20+
key: 'impact',
21+
isShownByDefault: true,
22+
isShown: true
23+
},
24+
{
25+
title: 'Score',
26+
key: 'score',
27+
isShownByDefault: false,
28+
isShown: false
29+
}
30+
];
31+
32+
export const ColumnExample: FunctionComponent = () => {
33+
const [ columns, setColumns ] = useState(DEFAULT_COLUMNS);
34+
35+
return (
36+
<ColumnManagement
37+
title="Manage columns"
38+
description="Select the columns to display in the table."
39+
columns={columns}
40+
onOrderChange={setColumns}
41+
onSelect={(col) => {
42+
const newColumns = [...columns];
43+
const changedColumn = newColumns.find(c => c.key === col.key);
44+
if (changedColumn) {
45+
changedColumn.isShown = col.isShown;
46+
}
47+
setColumns(newColumns);
48+
}}
49+
onSelectAll={(newColumns) => setColumns(newColumns)}
50+
onSave={(newColumns) => {
51+
setColumns(newColumns);
52+
alert('Changes saved!');
53+
}}
54+
onCancel={() => alert('Changes cancelled!')}
55+
/>
56+
);
57+
};

packages/module/src/Column/Column.test.tsx

Lines changed: 0 additions & 101 deletions
This file was deleted.

packages/module/src/Column/index.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import '@testing-library/jest-dom';
4+
import ColumnManagement from './ColumnManagement';
5+
6+
const mockColumns = [
7+
{ key: 'name', title: 'Name', isShown: true, isShownByDefault: true },
8+
{ key: 'status', title: 'Status', isShown: true, isShownByDefault: true },
9+
{ key: 'version', title: 'Version', isShown: false, isShownByDefault: false },
10+
];
11+
12+
describe('Column', () => {
13+
it('renders with initial columns', () => {
14+
render(<ColumnManagement columns={mockColumns} />);
15+
expect(screen.getByTestId('column-check-name')).toBeChecked();
16+
expect(screen.getByTestId('column-check-status')).toBeChecked();
17+
expect(screen.getByTestId('column-check-version')).not.toBeChecked();
18+
});
19+
20+
it('renders title and description', () => {
21+
render(<ColumnManagement columns={mockColumns} title="Test Title" description="Test Description" />);
22+
expect(screen.getByText('Test Title')).toBeInTheDocument();
23+
expect(screen.getByText('Test Description')).toBeInTheDocument();
24+
});
25+
26+
it('renders a cancel button', async () => {
27+
const onCancel = jest.fn();
28+
render(<ColumnManagement columns={mockColumns} onCancel={onCancel} />);
29+
const cancelButton = screen.getByText('Cancel');
30+
expect(cancelButton).toBeInTheDocument();
31+
await userEvent.click(cancelButton);
32+
expect(onCancel).toHaveBeenCalled();
33+
});
34+
35+
it('toggles a column', async () => {
36+
const onSelect = jest.fn();
37+
render(<ColumnManagement columns={mockColumns} onSelect={onSelect} />);
38+
const nameCheckbox = screen.getByTestId('column-check-name');
39+
await userEvent.click(nameCheckbox);
40+
expect(nameCheckbox).not.toBeChecked();
41+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: 'name', isShown: false }));
42+
});
43+
44+
it('selects all columns', async () => {
45+
render(<ColumnManagement columns={mockColumns} />);
46+
const menuToggle = screen.getByLabelText('Select all').closest('button');
47+
if (menuToggle) {
48+
await userEvent.click(menuToggle);
49+
}
50+
const selectAllButton = screen.getByText('Select all');
51+
await userEvent.click(selectAllButton);
52+
expect(screen.getByTestId('column-check-name')).toBeChecked();
53+
expect(screen.getByTestId('column-check-status')).toBeChecked();
54+
expect(screen.getByTestId('column-check-version')).toBeChecked();
55+
});
56+
57+
it('selects no columns', async () => {
58+
render(<ColumnManagement columns={mockColumns} />);
59+
const menuToggle = screen.getByLabelText('Select all').closest('button');
60+
if (menuToggle) {
61+
await userEvent.click(menuToggle);
62+
}
63+
const selectNoneButton = screen.getByText('Select none');
64+
await userEvent.click(selectNoneButton);
65+
expect(screen.getByTestId('column-check-name')).not.toBeChecked();
66+
expect(screen.getByTestId('column-check-status')).not.toBeChecked();
67+
expect(screen.getByTestId('column-check-version')).not.toBeChecked();
68+
});
69+
70+
it('saves changes', async () => {
71+
const onSave = jest.fn();
72+
render(<ColumnManagement columns={mockColumns} onSave={onSave} />);
73+
const saveButton = screen.getByText('Save');
74+
await userEvent.click(saveButton);
75+
expect(onSave).toHaveBeenCalledWith(expect.any(Array));
76+
});
77+
});

packages/module/src/Column/Column.tsx renamed to packages/module/src/ColumnManagement/ColumnManagement.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
Draggable
2424
} from '@patternfly/react-core/deprecated';
2525

26-
export interface ColumnColumn {
26+
export interface Column {
2727
/** Internal identifier of a column by which table displayed columns are filtered. */
2828
key: string;
2929
/** The actual display name of the column possibly with a tooltip or icon. */
@@ -38,29 +38,32 @@ export interface ColumnColumn {
3838

3939
export interface ColumnProps {
4040
/** Current column state */
41-
columns: ColumnColumn[];
41+
columns: Column[];
4242
/* Column description text */
4343
description?: string;
4444
/* Column title text */
4545
title?: string;
4646
/** Custom OUIA ID */
4747
ouiaId?: string | number;
4848
/** Callback when a column is selected or deselected */
49-
onSelect?: (column: ColumnColumn) => void;
49+
onSelect?: (column: Column) => void;
50+
/** Callback when all columns are selected or deselected */
51+
onSelectAll?: (columns: Column[]) => void;
5052
/** Callback when the column order changes */
51-
onOrderChange?: (columns: ColumnColumn[]) => void;
53+
onOrderChange?: (columns: Column[]) => void;
5254
/** Callback to save the column state */
53-
onSave?: (columns: ColumnColumn[]) => void;
55+
onSave?: (columns: Column[]) => void;
5456
/** Callback to close the modal */
5557
onCancel?: () => void;
5658
}
5759

58-
const Column: FunctionComponent<ColumnProps> = (
60+
const ColumnManagement: FunctionComponent<ColumnProps> = (
5961
{ columns,
6062
description,
6163
title,
6264
ouiaId = 'Column',
6365
onSelect,
66+
onSelectAll,
6467
onOrderChange,
6568
onSave,
6669
onCancel }: ColumnProps) => {
@@ -99,24 +102,23 @@ const Column: FunctionComponent<ColumnProps> = (
99102

100103
const handleSave = () => {
101104
onSave?.(currentColumns);
102-
onCancel?.();
103105
}
104106

105-
const onSelectAll = (select = true) => {
107+
const handleSelectAll = (select = true) => {
106108
const newColumns = currentColumns.map(c => ({ ...c, isShown: c.isUntoggleable ? c.isShown : select }));
107109
setCurrentColumns(newColumns);
108-
onOrderChange?.(newColumns);
110+
onSelectAll?.(newColumns);
109111
}
110112

111113
const isAllSelected = () => currentColumns.every(c => c.isShown || c.isUntoggleable);
112114
const isSomeSelected = () => currentColumns.some(c => c.isShown);
113115

114116
const dropdownItems = [
115-
<DropdownItem key="select-all" onClick={() => onSelectAll(true)}>Select all</DropdownItem>,
116-
<DropdownItem key="deselect-all" onClick={() => onSelectAll(false)}>Select none</DropdownItem>
117+
<DropdownItem key="select-all" onClick={() => handleSelectAll(true)}>Select all</DropdownItem>,
118+
<DropdownItem key="deselect-all" onClick={() => handleSelectAll(false)}>Select none</DropdownItem>
117119
];
118120

119-
const content = (
121+
return (
120122
<>
121123
<Title headingLevel="h3">{title}</Title>
122124
{description && <div style={{ paddingBottom: '1rem' }}><p>{description}</p></div>}
@@ -146,7 +148,7 @@ const Column: FunctionComponent<ColumnProps> = (
146148
<DataList aria-label="Selected columns" isCompact data-ouia-component-id={`${ouiaId}-column-list`}>
147149
{currentColumns.map((column, index) =>
148150
<Draggable key={column.key} id={column.key}>
149-
<DataListItem key={column.key}>
151+
<DataListItem key={column.key} data-testid={`column-item-${column.key}`}>
150152
<DataListItemRow>
151153
<DataListControl>
152154
<DataListDragButton
@@ -155,6 +157,7 @@ const Column: FunctionComponent<ColumnProps> = (
155157
/>
156158
</DataListControl>
157159
<DataListCheck
160+
data-testid={`column-check-${column.key}`}
158161
isChecked={column.isShown}
159162
onChange={() => handleChange(index)}
160163
isDisabled={column.isUntoggleable}
@@ -188,8 +191,6 @@ const Column: FunctionComponent<ColumnProps> = (
188191
</div>
189192
</>
190193
);
191-
192-
return content;
193194
}
194195

195-
export default Column;
196+
export default ColumnManagement;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ColumnManagement';
2+
export * from './ColumnManagement';

packages/module/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export * from './ErrorBoundary';
7272
export { default as ColumnManagementModal } from './ColumnManagementModal';
7373
export * from './ColumnManagementModal';
7474

75-
export { default as Column } from './Column';
76-
export * from './Column';
75+
export { default as ColumnManagement } from './ColumnManagement';
76+
export * from './ColumnManagement';
7777

7878
export { default as CloseButton } from './CloseButton';
7979
export * from './CloseButton';

0 commit comments

Comments
 (0)