-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add new Column Management Modal component (#129)
* 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
Showing
6 changed files
with
399 additions
and
0 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
...nsions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
``` |
96 changes: 96 additions & 0 deletions
96
...tensions/component-groups/examples/ColumnManagementModal/ColumnManagementModalExample.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
) | ||
} |
109 changes: 109 additions & 0 deletions
109
packages/module/src/ColumnManagementModal/ColumnManagementModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
159
packages/module/src/ColumnManagementModal/ColumnManagementModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.