diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md new file mode 100644 index 00000000..5b13aeeb --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModal.md @@ -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" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModalExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModalExample.tsx new file mode 100644 index 00000000..0e83f14a --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagementModal/ColumnManagementModalExample.tsx @@ -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 ( + <> + setColumns(newColumns)} + isOpen={isOpen} + onClose={() => setOpen(false)} + /> + + + + + {columns.filter(column => column.isShown).map(column => )} + + + + {ROWS.map((row, rowIndex) => + + {columns.filter(column => column.isShown).map((column, columnIndex) => + + )} + + )} + +
{column.title}
{row[column.key]}
+ + ) +} diff --git a/packages/module/src/ColumnManagementModal/ColumnManagementModal.test.tsx b/packages/module/src/ColumnManagementModal/ColumnManagementModal.test.tsx new file mode 100644 index 00000000..2ab91a54 --- /dev/null +++ b/packages/module/src/ColumnManagementModal/ColumnManagementModal.test.tsx @@ -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( 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); + }); +}); diff --git a/packages/module/src/ColumnManagementModal/ColumnManagementModal.tsx b/packages/module/src/ColumnManagementModal/ColumnManagementModal.tsx new file mode 100644 index 00000000..f2948b07 --- /dev/null +++ b/packages/module/src/ColumnManagementModal/ColumnManagementModal.tsx @@ -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 { + /** 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 = ( + { 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 ( + + {description} + + + + + + + + + + } + actions={[ + , + + ]} + ouiaId={ouiaId} + {...props} + > + + {currentColumns.map((column, index) => + + + handleChange(index)} + isDisabled={column.isUntoggleable} + aria-labelledby={`${ouiaId}-column${index}-label`} + data-ouia-component-id={`${ouiaId}-column${index}-checkbox`} + id={`${ouiaId}-column${index}-checkbox`} + /> + + + + ]} + /> + + + )} + + + ); +} + +export default ColumnManagementModal; diff --git a/packages/module/src/ColumnManagementModal/index.ts b/packages/module/src/ColumnManagementModal/index.ts new file mode 100644 index 00000000..b7bc77b7 --- /dev/null +++ b/packages/module/src/ColumnManagementModal/index.ts @@ -0,0 +1,2 @@ +export { default } from './ColumnManagementModal'; +export * from './ColumnManagementModal'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 93071ff6..a9bacb62 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -15,6 +15,9 @@ export * from './Battery'; export { default as CloseButton } from './CloseButton'; export * from './CloseButton'; +export { default as ColumnManagementModal } from './ColumnManagementModal'; +export * from './ColumnManagementModal'; + export { default as DetailsPage } from './DetailsPage'; export * from './DetailsPage';