Skip to content

Commit

Permalink
feat: Add new Column Management Modal component (#129)
Browse files Browse the repository at this point in the history
* feat: Add new Column Management Modal component

* Simplify example column names, use variant variables, use template literals

* Add ouiaIds

* Reuse modal props, add title and description props

* Rename `isAlwaysShown` column parameter to `isUntoggleable`

* Update packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md

Co-authored-by: Filip Hlavac <[email protected]>

* Update packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md

* Change example button variant to secondary

---------

Co-authored-by: Filip Hlavac <[email protected]>
  • Loading branch information
leSamo and fhlavac authored Apr 30, 2024
1 parent c4d7448 commit 7f9adb9
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# Sidenav top-level section
# should be the same for all markdown files
section: extensions
subsection: Component groups
# Sidenav secondary level section
# should be the same for all markdown files
id: Column management modal
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
propComponents: ['ColumnManagementModal']
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md
---

import ColumnManagementModal from '@patternfly/react-component-groups/dist/dynamic/ColumnManagementModal';
import { ColumnsIcon } from '@patternfly/react-icons';

The **column management modal** component can be used to implement customizable table columns. Columns can be configured to be enabled or disabled by default or be unhidable.

## Examples

### Showing and hiding of table columns

Clicking the "Manage columns" button will open the column management modal. The "ID" column should not be toggleable, therefore its checkbox is disabled with `isUntoggleable: true`. The "Score" column is set to be hidden by default. Always make sure to set `isShownByDefault` and `isShown` to the same boolean value in the initial state. For further customization, you can utilize all properties of the [modal component](/components/modal), except `ref` and `children`.

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

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react'
import { Button, ButtonVariant } from '@patternfly/react-core';
import { Table, Tbody, Td, Th, Tr, Thead } from '@patternfly/react-table';
import { ColumnsIcon } from '@patternfly/react-icons';
import ColumnManagementModal, { ColumnManagementModalColumn } from '@patternfly/react-component-groups/dist/dynamic/ColumnManagementModal';

const DEFAULT_COLUMNS: ColumnManagementModalColumn[] = [
{
title: 'ID',
key: 'id',
isShownByDefault: true,
isShown: true,
isUntoggleable: true
},
{
title: 'Publish date',
key: 'publishDate',
isShownByDefault: true,
isShown: true
},
{
title: 'Impact',
key: 'impact',
isShownByDefault: true,
isShown: true
},
{
title: 'Score',
key: 'score',
isShownByDefault: false,
isShown: false
}
];

const ROWS = [
{
id: 'CVE-2024-1546',
publishDate: '20 Feb 2024',
impact: 'Important',
score: '7.5'
},
{
id: 'CVE-2024-1547',
publishDate: '20 Feb 2024',
impact: 'Important',
score: '7.5'
},
{
id: 'CVE-2024-1548',
publishDate: '20 Feb 2024',
impact: 'Moderate',
score: '6.1'
},
{
id: 'CVE-2024-1549',
publishDate: '20 Feb 2024',
impact: 'Moderate',
score: '6.1'
}
]

export const ColumnManagementModalExample: React.FunctionComponent = () => {
const [ columns, setColumns ] = React.useState(DEFAULT_COLUMNS);
const [ isOpen, setOpen ] = React.useState(false);

return (
<>
<ColumnManagementModal
appliedColumns={columns}
applyColumns={newColumns => setColumns(newColumns)}
isOpen={isOpen}
onClose={() => setOpen(false)}
/>
<Button onClick={() => setOpen(true)} variant={ButtonVariant.secondary} icon={<ColumnsIcon />}>Manage columns</Button>
<Table
aria-label="Simple table"
variant="compact"
>
<Thead>
<Tr>
{columns.filter(column => column.isShown).map(column => <Th key={column.key}>{column.title}</Th>)}
</Tr>
</Thead>
<Tbody>
{ROWS.map((row, rowIndex) =>
<Tr key={rowIndex}>
{columns.filter(column => column.isShown).map((column, columnIndex) =>
<Td key={columnIndex}>{row[column.key]}</Td>
)}
</Tr>
)}
</Tbody>
</Table>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react';
import '@testing-library/jest-dom'
import { render, screen, fireEvent } from '@testing-library/react';
import ColumnManagementModal, { ColumnManagementModalColumn } from './ColumnManagementModal';

const DEFAULT_COLUMNS : ColumnManagementModalColumn[] = [
{
title: 'ID',
key: 'id',
isShownByDefault: true,
isShown: true,
isUntoggleable: true
},
{
title: 'Publish date',
key: 'publishDate',
isShownByDefault: true,
isShown: true
},
{
title: 'Impact',
key: 'impact',
isShownByDefault: true,
isShown: true
},
{
title: 'Score',
key: 'score',
isShownByDefault: false,
isShown: false
}
];

const onClose = jest.fn();
const setColumns = jest.fn();

beforeEach(() => {
render(<ColumnManagementModal
appliedColumns={DEFAULT_COLUMNS}
applyColumns={newColumns => setColumns(newColumns)}
isOpen
onClose={onClose}
data-testid="column-mgmt-modal"
/>);
});

const getCheckboxesState = () => {
const checkboxes = screen.getByTestId('column-mgmt-modal').querySelectorAll('input[type="checkbox"]');
return (Array.from(checkboxes) as HTMLInputElement[]).map(c => c.checked);
}

describe('ColumnManagementModal component', () => {
it('should have disabled and checked checkboxes for columns that should always be shown', () => {
expect(getCheckboxesState()).toEqual(DEFAULT_COLUMNS.map(c => c.isShownByDefault));
});

it('should have checkbox checked if column is shown by default', () => {
const idCheckbox = screen.getByTestId('column-mgmt-modal').querySelector('input[type="checkbox"][data-ouia-component-id="ColumnManagementModal-column0-checkbox"]');

expect(idCheckbox).toHaveAttribute('disabled');
expect(idCheckbox).toHaveAttribute('checked');
});

it('should set columns to default state upon clicking on "Reset to default"', () => {
// disable Impact column which is enabled by default
fireEvent.click(screen.getByText('Impact'));

// enable Score column which is disabled by default
fireEvent.click(screen.getByText('Score'));

fireEvent.click(screen.getByText('Reset to default'));

expect(getCheckboxesState()).toEqual(DEFAULT_COLUMNS.map(c => c.isShownByDefault));
});

it('should set all columns to show upon clicking on "Select all"', () => {
// disable Impact column which is enabled by default
fireEvent.click(screen.getByText('Impact'));

fireEvent.click(screen.getByText('Select all'));

expect(getCheckboxesState()).toEqual(DEFAULT_COLUMNS.map(_ => true));
});

it('should change columns on save', () => {
fireEvent.click(screen.getByText('Impact'));
fireEvent.click(screen.getByText('Save'));

const expectedColumns = DEFAULT_COLUMNS;
const impactColumn = expectedColumns.find(c => c.title === 'Impact');

if (impactColumn === undefined) {
throw new Error('Expected to find "Impact" column in "DEFAULT_COLUMNS"');
}

impactColumn.isShown = false;

expect(onClose).toHaveBeenCalled();
expect(setColumns).toHaveBeenCalledWith(expectedColumns);
});

it('should retain columns on cancel', () => {
fireEvent.click(screen.getByText('Impact'));
fireEvent.click(screen.getByText('Cancel'));

expect(onClose).toHaveBeenCalled();
expect(setColumns).toHaveBeenCalledWith(DEFAULT_COLUMNS);
});
});
159 changes: 159 additions & 0 deletions packages/module/src/ColumnManagementModal/ColumnManagementModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React from 'react';
import {
Modal,
Button,
TextContent,
Text,
TextVariants,
DataListItem,
DataList,
DataListItemRow,
DataListCheck,
DataListCell,
DataListItemCells,
Split,
SplitItem,
ModalProps,
ButtonVariant,
ModalVariant
} from '@patternfly/react-core';

export interface ColumnManagementModalColumn {
/** Internal identifier of a column by which table displayed columns are filtered. */
key: string,
/** The actual display name of the column possibly with a tooltip or icon. */
title: React.ReactNode,
/** If user changes checkboxes, the component will send back column array with this property altered. */
isShown?: boolean,
/** Set to false if the column should be hidden initially */
isShownByDefault: boolean,
/** The checkbox will be disabled, this is applicable to columns which should not be toggleable by user */
isUntoggleable?: boolean
}

export interface ColumnManagementModalProps extends Omit<ModalProps, 'ref' | 'children'> {
/** Flag to show the modal */
isOpen?: boolean,
/** Invoked when modal visibility is changed */
onClose?: (event: KeyboardEvent | React.MouseEvent) => void,
/** Current column state */
appliedColumns: ColumnManagementModalColumn[],
/** Invoked with new column state after save button is clicked */
applyColumns: (newColumns: ColumnManagementModalColumn[]) => void,
/* Modal description text */
description?: string,
/* Modal title text */
title?: string,
/** Custom OUIA ID */
ouiaId?: string | number,
};

const ColumnManagementModal: React.FunctionComponent<ColumnManagementModalProps> = (
{ title = 'Manage columns',
description = 'Selected categories will be displayed in the table.',
isOpen = false,
onClose = () => undefined,
appliedColumns,
applyColumns,
ouiaId = 'ColumnManagementModal',
...props }: ColumnManagementModalProps) => {

const [ currentColumns, setCurrentColumns ] = React.useState(
appliedColumns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault }))
);

const handleChange = index => {
const newColumns = [ ...currentColumns ];
const changedColumn = { ...newColumns[index] };

changedColumn.isShown = !changedColumn.isShown;
newColumns[index] = changedColumn;

setCurrentColumns(newColumns);
};

const selectAll = () => {
let newColumns = [ ...currentColumns ];
newColumns = newColumns.map(column => ({ ...column, isShown: true }));

setCurrentColumns(newColumns);
};

const resetToDefault = () => {
setCurrentColumns(currentColumns.map(column => ({ ...column, isShown: column.isShownByDefault ?? false })));
};

const handleSave = event => {
applyColumns(currentColumns);
onClose(event);
};

const handleCancel = event => {
setCurrentColumns(appliedColumns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault })));
onClose(event);
};

return (
<Modal
title={title}
onClose={onClose}
isOpen={isOpen}
variant={ModalVariant.small}
description={
<TextContent>
<Text component={TextVariants.p}>{description}</Text>
<Split hasGutter>
<SplitItem>
<Button isInline onClick={selectAll} variant={ButtonVariant.link} ouiaId={`${ouiaId}-selectAll-button`}>
Select all
</Button>
</SplitItem>
<SplitItem>
<Button isInline onClick={resetToDefault} variant={ButtonVariant.link} ouiaId={`${ouiaId}-reset-button`}>
Reset to default
</Button>
</SplitItem>
</Split>
</TextContent>
}
actions={[
<Button key="save" variant={ButtonVariant.primary} onClick={handleSave} ouiaId={`${ouiaId}-save-button`}>
Save
</Button>,
<Button key="cancel" variant={ButtonVariant.link} onClick={handleCancel} ouiaId={`${ouiaId}-cancel-button`}>
Cancel
</Button>
]}
ouiaId={ouiaId}
{...props}
>
<DataList aria-label="Selected columns" isCompact data-ouia-component-id={`${ouiaId}-column-list`}>
{currentColumns.map((column, index) =>
<DataListItem key={column.key}>
<DataListItemRow>
<DataListCheck
checked={column.isShown}
onChange={() => handleChange(index)}
isDisabled={column.isUntoggleable}
aria-labelledby={`${ouiaId}-column${index}-label`}
data-ouia-component-id={`${ouiaId}-column${index}-checkbox`}
id={`${ouiaId}-column${index}-checkbox`}
/>
<DataListItemCells
dataListCells={[
<DataListCell key={column.key} data-ouia-component-id={`${ouiaId}-column${index}-label`}>
<label htmlFor={`${ouiaId}-column${index}-checkbox`} id={`${ouiaId}-column${index}-label`}>
{column.title}
</label>
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
)}
</DataList>
</Modal>
);
}

export default ColumnManagementModal;
Loading

0 comments on commit 7f9adb9

Please sign in to comment.