From 1c1a4ed4bf02e3a71960c5097784b5aeca0fe6f7 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Mon, 14 Jul 2025 17:26:33 -0400 Subject: [PATCH 01/10] feat: add drag and drop column component --- packages/module/src/Column/Column.test.tsx | 101 +++++++++++ packages/module/src/Column/Column.tsx | 195 +++++++++++++++++++++ packages/module/src/Column/index.ts | 2 + packages/module/src/index.ts | 3 + 4 files changed, 301 insertions(+) create mode 100644 packages/module/src/Column/Column.test.tsx create mode 100644 packages/module/src/Column/Column.tsx create mode 100644 packages/module/src/Column/index.ts diff --git a/packages/module/src/Column/Column.test.tsx b/packages/module/src/Column/Column.test.tsx new file mode 100644 index 00000000..7c30eaca --- /dev/null +++ b/packages/module/src/Column/Column.test.tsx @@ -0,0 +1,101 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import Column from '../Column'; + +const mockColumns = [ + { key: 'name', title: 'Name', isShownByDefault: true }, + { key: 'status', title: 'Status', isShownByDefault: true }, + { key: 'version', title: 'Version', isShownByDefault: false }, +]; + +describe('Column', () => { + it('renders with initial columns', () => { + render(); + expect(screen.getByLabelText('Name')).toBeChecked(); + expect(screen.getByLabelText('Status')).toBeChecked(); + expect(screen.getByLabelText('Version')).not.toBeChecked(); + }); + + it('renders title and description', () => { + render(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Description')).toBeInTheDocument(); + }); + + it('renders a cancel button', () => { + const onCancel = jest.fn(); + render(); + const cancelButton = screen.getByText('Cancel'); + expect(cancelButton).toBeInTheDocument(); + userEvent.click(cancelButton); + expect(onCancel).toHaveBeenCalled(); + }); + + it('toggles a column', async () => { + const onSelect = jest.fn(); + render(); + const nameCheckbox = screen.getByLabelText('Name'); + await userEvent.click(nameCheckbox); + expect(nameCheckbox).not.toBeChecked(); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: 'name', isShown: false })); + }); + + it('selects all columns', async () => { + render(); + const menuToggle = screen.getByLabelText('Select all').closest('button'); + if (menuToggle) { + await userEvent.click(menuToggle); + } + const selectAllButton = screen.getByText('Select all'); + await userEvent.click(selectAllButton); + expect(screen.getByLabelText('Name')).toBeChecked(); + expect(screen.getByLabelText('Status')).toBeChecked(); + expect(screen.getByLabelText('Version')).toBeChecked(); + }); + + it('selects no columns', async () => { + render(); + const menuToggle = screen.getByLabelText('Select all').closest('button'); + if (menuToggle) { + await userEvent.click(menuToggle); + } + const selectNoneButton = screen.getByText('Select none'); + await userEvent.click(selectNoneButton); + expect(screen.getByLabelText('Name')).not.toBeChecked(); + expect(screen.getByLabelText('Status')).not.toBeChecked(); + expect(screen.getByLabelText('Version')).not.toBeChecked(); + }); + + it('saves changes', async () => { + const onSave = jest.fn(); + render(); + const saveButton = screen.getByText('Save'); + await userEvent.click(saveButton); + expect(onSave).toHaveBeenCalledWith(expect.any(Array)); + }); + + it('reorders columns with drag and drop', () => { + const onOrderChange = jest.fn(); + const { container } = render(); + const firstItem = screen.getByText('Name').closest('li'); + const secondItem = screen.getByText('Status').closest('li'); + + if (firstItem && secondItem) { + fireEvent.dragStart(firstItem); + fireEvent.dragEnter(secondItem); + fireEvent.dragOver(secondItem); + fireEvent.drop(secondItem); + fireEvent.dragEnd(firstItem); + + const listItems = container.querySelectorAll('li'); + expect(listItems[0].textContent).toContain('Status'); + expect(listItems[1].textContent).toContain('Name'); + expect(onOrderChange).toHaveBeenCalledWith([ + expect.objectContaining({ key: 'status' }), + expect.objectContaining({ key: 'name' }), + expect.objectContaining({ key: 'version' }), + ]); + } + }); +}); diff --git a/packages/module/src/Column/Column.tsx b/packages/module/src/Column/Column.tsx new file mode 100644 index 00000000..09b41890 --- /dev/null +++ b/packages/module/src/Column/Column.tsx @@ -0,0 +1,195 @@ +import type { FunctionComponent } from 'react'; +import { useState, useEffect } from 'react'; +import { + DataListItem, + DataList, + DataListItemRow, + DataListCheck, + DataListCell, + DataListItemCells, + DataListControl, + DataListDragButton, + Button, + ButtonVariant, + Title, + Checkbox, + Dropdown, + DropdownItem, + MenuToggle +} from '@patternfly/react-core'; +import { + DragDrop, + Droppable, + Draggable +} from '@patternfly/react-core/deprecated'; + +export interface ColumnColumn { + /** 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 ColumnProps { + /** Current column state */ + columns: ColumnColumn[]; + /* Column description text */ + description?: string; + /* Column title text */ + title?: string; + /** Custom OUIA ID */ + ouiaId?: string | number; + /** Callback when a column is selected or deselected */ + onSelect?: (column: ColumnColumn) => void; + /** Callback when the column order changes */ + onOrderChange?: (columns: ColumnColumn[]) => void; + /** Callback to save the column state */ + onSave?: (columns: ColumnColumn[]) => void; + /** Callback to close the modal */ + onCancel?: () => void; +} + +const Column: FunctionComponent = ( + { columns, + description, + title, + ouiaId = 'Column', + onSelect, + onOrderChange, + onSave, + onCancel }: ColumnProps) => { + + const [ isDropdownOpen, setIsDropdownOpen ] = useState(false); + const [ currentColumns, setCurrentColumns ] = useState( + () => columns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault, id: column.key })) + ); + + useEffect(() => { + setCurrentColumns(columns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault, id: column.key }))); + }, [ columns ]); + + const handleChange = index => { + const newColumns = [ ...currentColumns ]; + const changedColumn = { ...newColumns[index] }; + + changedColumn.isShown = !changedColumn.isShown; + newColumns[index] = changedColumn; + + setCurrentColumns(newColumns); + onSelect?.(changedColumn); + }; + + const onDrag = (source, dest) => { + if (dest) { + const newColumns = [ ...currentColumns ]; + const [ removed ] = newColumns.splice(source.index, 1); + newColumns.splice(dest.index, 0, removed); + setCurrentColumns(newColumns); + onOrderChange?.(newColumns); + return true; + } + return false; + }; + + const handleSave = () => { + onSave?.(currentColumns); + onCancel?.(); + } + + const onSelectAll = (select = true) => { + const newColumns = currentColumns.map(c => ({ ...c, isShown: c.isUntoggleable ? c.isShown : select })); + setCurrentColumns(newColumns); + onOrderChange?.(newColumns); + } + + const isAllSelected = () => currentColumns.every(c => c.isShown || c.isUntoggleable); + const isSomeSelected = () => currentColumns.some(c => c.isShown); + + const dropdownItems = [ + onSelectAll(true)}>Select all, + onSelectAll(false)}>Select none + ]; + + const content = ( + <> + {title} + {description &&

{description}

} +
+ setIsDropdownOpen(false)} + toggle={(toggleRef) => ( + setIsDropdownOpen(!isDropdownOpen)} + isExpanded={isDropdownOpen} + > + + + )} + isOpen={isDropdownOpen} + > + {dropdownItems} + +
+ + + + {currentColumns.map((column, index) => + + + + + + + handleChange(index)} + isDisabled={column.isUntoggleable} + aria-labelledby={`${ouiaId}-column-${index}-label`} + ouiaId={`${ouiaId}-column-${index}-checkbox`} + id={`${ouiaId}-column-${index}-checkbox`} + /> + + + + ]} + /> + + + + )} + + + +
+ + +
+ + ); + + return content; +} + +export default Column; diff --git a/packages/module/src/Column/index.ts b/packages/module/src/Column/index.ts new file mode 100644 index 00000000..8f5de7e6 --- /dev/null +++ b/packages/module/src/Column/index.ts @@ -0,0 +1,2 @@ +export { default } from './Column'; +export * from './Column'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 640a3cee..3ac16b0c 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -72,6 +72,9 @@ export * from './ErrorBoundary'; export { default as ColumnManagementModal } from './ColumnManagementModal'; export * from './ColumnManagementModal'; +export { default as Column } from './Column'; +export * from './Column'; + export { default as CloseButton } from './CloseButton'; export * from './CloseButton'; From ab3fc5aa5014bc1450c1a302b5c1305c0852516a Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Wed, 16 Jul 2025 16:01:32 -0400 Subject: [PATCH 02/10] feat: refactor Column into ColumnManagement and add patternfly-docs/examples --- .../ColumnManagement/ColumnManagement.md | 28 +++++ .../ColumnManagementExample.tsx | 57 ++++++++++ packages/module/src/Column/Column.test.tsx | 101 ------------------ packages/module/src/Column/index.ts | 2 - .../ColumnManagement.test.tsx | 77 +++++++++++++ .../ColumnManagement.tsx} | 33 +++--- packages/module/src/ColumnManagement/index.ts | 2 + packages/module/src/index.ts | 4 +- 8 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx delete mode 100644 packages/module/src/Column/Column.test.tsx delete mode 100644 packages/module/src/Column/index.ts create mode 100644 packages/module/src/ColumnManagement/ColumnManagement.test.tsx rename packages/module/src/{Column/Column.tsx => ColumnManagement/ColumnManagement.tsx} (87%) create mode 100644 packages/module/src/ColumnManagement/index.ts diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md new file mode 100644 index 00000000..40de02c8 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md @@ -0,0 +1,28 @@ +--- +# Sidenav top-level section +# should be the same for all markdown files +section: Component groups +subsection: Helpers +# Sidenav secondary level section +# should be the same for all markdown files +id: Column management +# 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: ['ColumnManagement'] +sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md +--- + +import ColumnManagement from '@patternfly/react-component-groups/dist/dynamic/ColumnManagement'; +import { FunctionComponent, useState } from 'react'; + +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. + +## Examples + +### Basic column list + +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. + +```js file="./ColumnManagementExample.tsx" diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx new file mode 100644 index 00000000..16e02420 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx @@ -0,0 +1,57 @@ +import { FunctionComponent, useState } from 'react'; +import { Column, ColumnManagement } from '@patternfly/react-component-groups'; + +const DEFAULT_COLUMNS: Column[] = [ + { + 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 + } +]; + +export const ColumnExample: FunctionComponent = () => { + const [ columns, setColumns ] = useState(DEFAULT_COLUMNS); + + return ( + { + const newColumns = [...columns]; + const changedColumn = newColumns.find(c => c.key === col.key); + if (changedColumn) { + changedColumn.isShown = col.isShown; + } + setColumns(newColumns); + }} + onSelectAll={(newColumns) => setColumns(newColumns)} + onSave={(newColumns) => { + setColumns(newColumns); + alert('Changes saved!'); + }} + onCancel={() => alert('Changes cancelled!')} + /> + ); +}; diff --git a/packages/module/src/Column/Column.test.tsx b/packages/module/src/Column/Column.test.tsx deleted file mode 100644 index 7c30eaca..00000000 --- a/packages/module/src/Column/Column.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom'; -import Column from '../Column'; - -const mockColumns = [ - { key: 'name', title: 'Name', isShownByDefault: true }, - { key: 'status', title: 'Status', isShownByDefault: true }, - { key: 'version', title: 'Version', isShownByDefault: false }, -]; - -describe('Column', () => { - it('renders with initial columns', () => { - render(); - expect(screen.getByLabelText('Name')).toBeChecked(); - expect(screen.getByLabelText('Status')).toBeChecked(); - expect(screen.getByLabelText('Version')).not.toBeChecked(); - }); - - it('renders title and description', () => { - render(); - expect(screen.getByText('Test Title')).toBeInTheDocument(); - expect(screen.getByText('Test Description')).toBeInTheDocument(); - }); - - it('renders a cancel button', () => { - const onCancel = jest.fn(); - render(); - const cancelButton = screen.getByText('Cancel'); - expect(cancelButton).toBeInTheDocument(); - userEvent.click(cancelButton); - expect(onCancel).toHaveBeenCalled(); - }); - - it('toggles a column', async () => { - const onSelect = jest.fn(); - render(); - const nameCheckbox = screen.getByLabelText('Name'); - await userEvent.click(nameCheckbox); - expect(nameCheckbox).not.toBeChecked(); - expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: 'name', isShown: false })); - }); - - it('selects all columns', async () => { - render(); - const menuToggle = screen.getByLabelText('Select all').closest('button'); - if (menuToggle) { - await userEvent.click(menuToggle); - } - const selectAllButton = screen.getByText('Select all'); - await userEvent.click(selectAllButton); - expect(screen.getByLabelText('Name')).toBeChecked(); - expect(screen.getByLabelText('Status')).toBeChecked(); - expect(screen.getByLabelText('Version')).toBeChecked(); - }); - - it('selects no columns', async () => { - render(); - const menuToggle = screen.getByLabelText('Select all').closest('button'); - if (menuToggle) { - await userEvent.click(menuToggle); - } - const selectNoneButton = screen.getByText('Select none'); - await userEvent.click(selectNoneButton); - expect(screen.getByLabelText('Name')).not.toBeChecked(); - expect(screen.getByLabelText('Status')).not.toBeChecked(); - expect(screen.getByLabelText('Version')).not.toBeChecked(); - }); - - it('saves changes', async () => { - const onSave = jest.fn(); - render(); - const saveButton = screen.getByText('Save'); - await userEvent.click(saveButton); - expect(onSave).toHaveBeenCalledWith(expect.any(Array)); - }); - - it('reorders columns with drag and drop', () => { - const onOrderChange = jest.fn(); - const { container } = render(); - const firstItem = screen.getByText('Name').closest('li'); - const secondItem = screen.getByText('Status').closest('li'); - - if (firstItem && secondItem) { - fireEvent.dragStart(firstItem); - fireEvent.dragEnter(secondItem); - fireEvent.dragOver(secondItem); - fireEvent.drop(secondItem); - fireEvent.dragEnd(firstItem); - - const listItems = container.querySelectorAll('li'); - expect(listItems[0].textContent).toContain('Status'); - expect(listItems[1].textContent).toContain('Name'); - expect(onOrderChange).toHaveBeenCalledWith([ - expect.objectContaining({ key: 'status' }), - expect.objectContaining({ key: 'name' }), - expect.objectContaining({ key: 'version' }), - ]); - } - }); -}); diff --git a/packages/module/src/Column/index.ts b/packages/module/src/Column/index.ts deleted file mode 100644 index 8f5de7e6..00000000 --- a/packages/module/src/Column/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Column'; -export * from './Column'; diff --git a/packages/module/src/ColumnManagement/ColumnManagement.test.tsx b/packages/module/src/ColumnManagement/ColumnManagement.test.tsx new file mode 100644 index 00000000..dadf3bb4 --- /dev/null +++ b/packages/module/src/ColumnManagement/ColumnManagement.test.tsx @@ -0,0 +1,77 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import ColumnManagement from './ColumnManagement'; + +const mockColumns = [ + { key: 'name', title: 'Name', isShown: true, isShownByDefault: true }, + { key: 'status', title: 'Status', isShown: true, isShownByDefault: true }, + { key: 'version', title: 'Version', isShown: false, isShownByDefault: false }, +]; + +describe('Column', () => { + it('renders with initial columns', () => { + render(); + expect(screen.getByTestId('column-check-name')).toBeChecked(); + expect(screen.getByTestId('column-check-status')).toBeChecked(); + expect(screen.getByTestId('column-check-version')).not.toBeChecked(); + }); + + it('renders title and description', () => { + render(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Description')).toBeInTheDocument(); + }); + + it('renders a cancel button', async () => { + const onCancel = jest.fn(); + render(); + const cancelButton = screen.getByText('Cancel'); + expect(cancelButton).toBeInTheDocument(); + await userEvent.click(cancelButton); + expect(onCancel).toHaveBeenCalled(); + }); + + it('toggles a column', async () => { + const onSelect = jest.fn(); + render(); + const nameCheckbox = screen.getByTestId('column-check-name'); + await userEvent.click(nameCheckbox); + expect(nameCheckbox).not.toBeChecked(); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: 'name', isShown: false })); + }); + + it('selects all columns', async () => { + render(); + const menuToggle = screen.getByLabelText('Select all').closest('button'); + if (menuToggle) { + await userEvent.click(menuToggle); + } + const selectAllButton = screen.getByText('Select all'); + await userEvent.click(selectAllButton); + expect(screen.getByTestId('column-check-name')).toBeChecked(); + expect(screen.getByTestId('column-check-status')).toBeChecked(); + expect(screen.getByTestId('column-check-version')).toBeChecked(); + }); + + it('selects no columns', async () => { + render(); + const menuToggle = screen.getByLabelText('Select all').closest('button'); + if (menuToggle) { + await userEvent.click(menuToggle); + } + const selectNoneButton = screen.getByText('Select none'); + await userEvent.click(selectNoneButton); + expect(screen.getByTestId('column-check-name')).not.toBeChecked(); + expect(screen.getByTestId('column-check-status')).not.toBeChecked(); + expect(screen.getByTestId('column-check-version')).not.toBeChecked(); + }); + + it('saves changes', async () => { + const onSave = jest.fn(); + render(); + const saveButton = screen.getByText('Save'); + await userEvent.click(saveButton); + expect(onSave).toHaveBeenCalledWith(expect.any(Array)); + }); +}); diff --git a/packages/module/src/Column/Column.tsx b/packages/module/src/ColumnManagement/ColumnManagement.tsx similarity index 87% rename from packages/module/src/Column/Column.tsx rename to packages/module/src/ColumnManagement/ColumnManagement.tsx index 09b41890..680d486e 100644 --- a/packages/module/src/Column/Column.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.tsx @@ -23,7 +23,7 @@ import { Draggable } from '@patternfly/react-core/deprecated'; -export interface ColumnColumn { +export interface Column { /** 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. */ @@ -38,7 +38,7 @@ export interface ColumnColumn { export interface ColumnProps { /** Current column state */ - columns: ColumnColumn[]; + columns: Column[]; /* Column description text */ description?: string; /* Column title text */ @@ -46,21 +46,24 @@ export interface ColumnProps { /** Custom OUIA ID */ ouiaId?: string | number; /** Callback when a column is selected or deselected */ - onSelect?: (column: ColumnColumn) => void; + onSelect?: (column: Column) => void; + /** Callback when all columns are selected or deselected */ + onSelectAll?: (columns: Column[]) => void; /** Callback when the column order changes */ - onOrderChange?: (columns: ColumnColumn[]) => void; + onOrderChange?: (columns: Column[]) => void; /** Callback to save the column state */ - onSave?: (columns: ColumnColumn[]) => void; + onSave?: (columns: Column[]) => void; /** Callback to close the modal */ onCancel?: () => void; } -const Column: FunctionComponent = ( +const ColumnManagement: FunctionComponent = ( { columns, description, title, ouiaId = 'Column', onSelect, + onSelectAll, onOrderChange, onSave, onCancel }: ColumnProps) => { @@ -99,24 +102,23 @@ const Column: FunctionComponent = ( const handleSave = () => { onSave?.(currentColumns); - onCancel?.(); } - const onSelectAll = (select = true) => { + const handleSelectAll = (select = true) => { const newColumns = currentColumns.map(c => ({ ...c, isShown: c.isUntoggleable ? c.isShown : select })); setCurrentColumns(newColumns); - onOrderChange?.(newColumns); + onSelectAll?.(newColumns); } const isAllSelected = () => currentColumns.every(c => c.isShown || c.isUntoggleable); const isSomeSelected = () => currentColumns.some(c => c.isShown); const dropdownItems = [ - onSelectAll(true)}>Select all, - onSelectAll(false)}>Select none + handleSelectAll(true)}>Select all, + handleSelectAll(false)}>Select none ]; - const content = ( + return ( <> {title} {description &&

{description}

} @@ -146,7 +148,7 @@ const Column: FunctionComponent = ( {currentColumns.map((column, index) => - + = ( /> handleChange(index)} isDisabled={column.isUntoggleable} @@ -188,8 +191,6 @@ const Column: FunctionComponent = ( ); - - return content; } -export default Column; +export default ColumnManagement; diff --git a/packages/module/src/ColumnManagement/index.ts b/packages/module/src/ColumnManagement/index.ts new file mode 100644 index 00000000..283ecbe7 --- /dev/null +++ b/packages/module/src/ColumnManagement/index.ts @@ -0,0 +1,2 @@ +export { default } from './ColumnManagement'; +export * from './ColumnManagement'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 3ac16b0c..54683748 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -72,8 +72,8 @@ export * from './ErrorBoundary'; export { default as ColumnManagementModal } from './ColumnManagementModal'; export * from './ColumnManagementModal'; -export { default as Column } from './Column'; -export * from './Column'; +export { default as ColumnManagement } from './ColumnManagement'; +export * from './ColumnManagement'; export { default as CloseButton } from './CloseButton'; export * from './CloseButton'; From 15a0f55fb3f0bcfd447029a83e20bbbfbd3f0f93 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Wed, 23 Jul 2025 14:03:44 -0400 Subject: [PATCH 03/10] fix: address column accessibility issues --- .../src/ColumnManagement/ColumnManagement.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/module/src/ColumnManagement/ColumnManagement.tsx b/packages/module/src/ColumnManagement/ColumnManagement.tsx index 680d486e..c3c9c4fb 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.tsx @@ -15,6 +15,7 @@ import { Checkbox, Dropdown, DropdownItem, + DropdownList, MenuToggle } from '@patternfly/react-core'; import { @@ -131,16 +132,19 @@ const ColumnManagement: FunctionComponent = ( onClick={() => setIsDropdownOpen(!isDropdownOpen)} isExpanded={isDropdownOpen} > - +
+ +
)} isOpen={isDropdownOpen} > - {dropdownItems} + {dropdownItems} From 7ca18a6f9d24751f2ea1f3217d44e28c561b8229 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Wed, 23 Jul 2025 15:06:47 -0400 Subject: [PATCH 04/10] fix: add ul for DataListItem --- .../src/ColumnManagement/ColumnManagement.tsx | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/module/src/ColumnManagement/ColumnManagement.tsx b/packages/module/src/ColumnManagement/ColumnManagement.tsx index c3c9c4fb..d5d52ce3 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.tsx @@ -152,34 +152,36 @@ const ColumnManagement: FunctionComponent = ( {currentColumns.map((column, index) => - - - - + + + + + + handleChange(index)} + isDisabled={column.isUntoggleable} aria-labelledby={`${ouiaId}-column-${index}-label`} + ouiaId={`${ouiaId}-column-${index}-checkbox`} + id={`${ouiaId}-column-${index}-checkbox`} /> - - handleChange(index)} - isDisabled={column.isUntoggleable} - aria-labelledby={`${ouiaId}-column-${index}-label`} - ouiaId={`${ouiaId}-column-${index}-checkbox`} - id={`${ouiaId}-column-${index}-checkbox`} - /> - - - - ]} - /> - - + + + + ]} + /> +
+
+
)}
From 59f4d823a39fef50bd1861d61a6e2b85bb425d4d Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Wed, 23 Jul 2025 15:53:48 -0400 Subject: [PATCH 05/10] chore: revert ul change --- .../src/ColumnManagement/ColumnManagement.tsx | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/module/src/ColumnManagement/ColumnManagement.tsx b/packages/module/src/ColumnManagement/ColumnManagement.tsx index d5d52ce3..c3c9c4fb 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.tsx @@ -152,36 +152,34 @@ const ColumnManagement: FunctionComponent = ( {currentColumns.map((column, index) => -
    - - - - - - handleChange(index)} - isDisabled={column.isUntoggleable} + + + + - - - - ]} - /> - - -
+ + handleChange(index)} + isDisabled={column.isUntoggleable} + aria-labelledby={`${ouiaId}-column-${index}-label`} + ouiaId={`${ouiaId}-column-${index}-checkbox`} + id={`${ouiaId}-column-${index}-checkbox`} + /> + + + + ]} + /> + +
)}
From edfe9b41568fe94290421d58c71fa1eae2abd172 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Fri, 25 Jul 2025 15:31:47 -0400 Subject: [PATCH 06/10] fix: implement DragDropSort and BulkSelect --- .../ColumnManagement.test.tsx | 22 ++- .../src/ColumnManagement/ColumnManagement.tsx | 152 +++++++----------- 2 files changed, 76 insertions(+), 98 deletions(-) diff --git a/packages/module/src/ColumnManagement/ColumnManagement.test.tsx b/packages/module/src/ColumnManagement/ColumnManagement.test.tsx index dadf3bb4..9cb4fa7c 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.test.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.test.tsx @@ -3,6 +3,20 @@ import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import ColumnManagement from './ColumnManagement'; +jest.mock('@patternfly/react-drag-drop', () => { + const originalModule = jest.requireActual('@patternfly/react-drag-drop'); + return { + ...originalModule, + DragDropSort: ({ onDrop, items }) => { + const handleDrop = () => { + const reorderedItems = [ ...items ].reverse(); + onDrop({}, reorderedItems); + }; + return
{items.map(item => item.content)}
; + }, + }; +}); + const mockColumns = [ { key: 'name', title: 'Name', isShown: true, isShownByDefault: true }, { key: 'status', title: 'Status', isShown: true, isShownByDefault: true }, @@ -43,11 +57,11 @@ describe('Column', () => { it('selects all columns', async () => { render(); - const menuToggle = screen.getByLabelText('Select all').closest('button'); + const menuToggle = screen.getByLabelText('Bulk select toggle'); if (menuToggle) { await userEvent.click(menuToggle); } - const selectAllButton = screen.getByText('Select all'); + const selectAllButton = screen.getByText('Select all (3)'); await userEvent.click(selectAllButton); expect(screen.getByTestId('column-check-name')).toBeChecked(); expect(screen.getByTestId('column-check-status')).toBeChecked(); @@ -56,11 +70,11 @@ describe('Column', () => { it('selects no columns', async () => { render(); - const menuToggle = screen.getByLabelText('Select all').closest('button'); + const menuToggle = screen.getByLabelText('Bulk select toggle'); if (menuToggle) { await userEvent.click(menuToggle); } - const selectNoneButton = screen.getByText('Select none'); + const selectNoneButton = screen.getByText('Select none (0)'); await userEvent.click(selectNoneButton); expect(screen.getByTestId('column-check-name')).not.toBeChecked(); expect(screen.getByTestId('column-check-status')).not.toBeChecked(); diff --git a/packages/module/src/ColumnManagement/ColumnManagement.tsx b/packages/module/src/ColumnManagement/ColumnManagement.tsx index c3c9c4fb..13b79072 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.tsx +++ b/packages/module/src/ColumnManagement/ColumnManagement.tsx @@ -1,28 +1,17 @@ import type { FunctionComponent } from 'react'; import { useState, useEffect } from 'react'; import { - DataListItem, DataList, DataListItemRow, DataListCheck, DataListCell, DataListItemCells, - DataListControl, - DataListDragButton, Button, ButtonVariant, - Title, - Checkbox, - Dropdown, - DropdownItem, - DropdownList, - MenuToggle + Title } from '@patternfly/react-core'; -import { - DragDrop, - Droppable, - Draggable -} from '@patternfly/react-core/deprecated'; +import { DragDropSort, Droppable } from '@patternfly/react-drag-drop'; +import BulkSelect, { BulkSelectValue } from '../BulkSelect'; export interface Column { /** Internal identifier of a column by which table displayed columns are filtered. */ @@ -69,7 +58,6 @@ const ColumnManagement: FunctionComponent = ( onSave, onCancel }: ColumnProps) => { - const [ isDropdownOpen, setIsDropdownOpen ] = useState(false); const [ currentColumns, setCurrentColumns ] = useState( () => columns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault, id: column.key })) ); @@ -89,103 +77,79 @@ const ColumnManagement: FunctionComponent = ( onSelect?.(changedColumn); }; - const onDrag = (source, dest) => { - if (dest) { - const newColumns = [ ...currentColumns ]; - const [ removed ] = newColumns.splice(source.index, 1); - newColumns.splice(dest.index, 0, removed); - setCurrentColumns(newColumns); - onOrderChange?.(newColumns); - return true; - } - return false; + const onDrag = (_event, newOrder) => { + const newColumns = newOrder.map(item => currentColumns.find(c => c.key === item.id)); + setCurrentColumns(newColumns); + onOrderChange?.(newColumns); }; const handleSave = () => { onSave?.(currentColumns); } + const handleBulkSelect = (value: BulkSelectValue) => { + const allSelected = value === 'all' || value === 'page'; + handleSelectAll(allSelected); + }; + const handleSelectAll = (select = true) => { const newColumns = currentColumns.map(c => ({ ...c, isShown: c.isUntoggleable ? c.isShown : select })); setCurrentColumns(newColumns); onSelectAll?.(newColumns); } - const isAllSelected = () => currentColumns.every(c => c.isShown || c.isUntoggleable); - const isSomeSelected = () => currentColumns.some(c => c.isShown); - - const dropdownItems = [ - handleSelectAll(true)}>Select all, - handleSelectAll(false)}>Select none - ]; - return ( <> {title} {description &&

{description}

}
- setIsDropdownOpen(false)} - toggle={(toggleRef) => ( - setIsDropdownOpen(!isDropdownOpen)} - isExpanded={isDropdownOpen} - > -
- -
-
- )} - isOpen={isDropdownOpen} - > - {dropdownItems} -
+ isShown).length} + totalCount={currentColumns.length} + onSelect={handleBulkSelect} + pageSelected={currentColumns.every((item) => item.isShown)} + pagePartiallySelected={ + currentColumns.some((item) => item.isShown) && !currentColumns.every((item) => item.isShown) + } + />
- - - - {currentColumns.map((column, index) => - - - - - - - handleChange(index)} - isDisabled={column.isUntoggleable} - aria-labelledby={`${ouiaId}-column-${index}-label`} - ouiaId={`${ouiaId}-column-${index}-checkbox`} - id={`${ouiaId}-column-${index}-checkbox`} - /> - - - - ]} - /> - - - - )} - - - -
+ ({ id: column.key, content: + + handleChange(index)} + isDisabled={column.isUntoggleable} + aria-labelledby={`${ouiaId}-column-${index}-label`} + ouiaId={`${ouiaId}-column-${index}-checkbox`} + id={`${ouiaId}-column-${index}-checkbox`} + /> + + + + ]} + /> + + }))} + onDrop={onDrag} + overlayProps={{ isCompact: true }} + > + + // eslint-disable-next-line no-console + ({ id: column.key, content: column.title }) + )} + wrapper={} + /> +
From cedab1c1fd2a748e082da97a72f0e9b4f8773f27 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Fri, 25 Jul 2025 15:33:19 -0400 Subject: [PATCH 07/10] chore: add react-drag-drop package --- package-lock.json | 131 +++++++++++++++++++++++++++++++++++++++------- package.json | 1 + 2 files changed, 113 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4d4f8f1..fdfd7acf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/*" ], "dependencies": { + "@patternfly/react-drag-drop": "^6.3.0", "@patternfly/react-tokens": "^6.0.0", "sharp": "^0.33.5" }, @@ -1983,6 +1984,73 @@ "node": ">=10.0.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", @@ -4400,37 +4468,56 @@ "link": true }, "node_modules/@patternfly/react-core": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.2.2.tgz", - "integrity": "sha512-JUrZ57JQ4bkmed1kxaciXb0ZpIVYyCHc2HjtzoKQ5UNRlx204zR2isATSHjdw2GFcWvwpkC5/fU2BR+oT3opbg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.3.0.tgz", + "integrity": "sha512-TM+pLwLd5DzaDlOQhqeju9H9QUFQypQiNwXQLNIxOV5r3fmKh4NTp2Av/8WmFkpCj8mejDOfp4TNxoU1zdjCkQ==", "license": "MIT", "dependencies": { - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", - "@patternfly/react-tokens": "^6.2.2", + "@patternfly/react-icons": "^6.3.0", + "@patternfly/react-styles": "^6.3.0", + "@patternfly/react-tokens": "^6.3.0", "focus-trap": "7.6.4", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, + "node_modules/@patternfly/react-drag-drop": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.3.0.tgz", + "integrity": "sha512-6MgH1ZoMmugw9ESWO8D2z2Xc9v9kQTCfoQJRifH3PoC7IW0047yw/6vHnLonLxfeBkx5QR/zVmYnKyWNd+Q5OQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@patternfly/react-core": "^6.3.0", + "@patternfly/react-icons": "^6.3.0", + "@patternfly/react-styles": "^6.3.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-icons": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.2.2.tgz", - "integrity": "sha512-XkBwzuV/uiolX+T6QgB3RIqphM1m+vAZjAe3McYtyY22j1rsOdlWDE4RtRrJ1q7EoIZwyZHj0h8T9vMfUsLn4Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.3.0.tgz", + "integrity": "sha512-W39JyqKW1UL6/YGuinDnpjbhmmLAfuxVrgDcdFBaK4D7D1iqkkqrDMV8zIzmV/RkodJ79xRnucYhYb2RukG4RA==", "license": "MIT", "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-styles": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.2.2.tgz", - "integrity": "sha512-rncRDq66H8VnLyb9DrHHlZtPddlpNL9+W0XuQC0L7F6p78hOwSZmoGTW2Vq8/wJplDj8h/61qRpfRF9VEYPW0g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.3.0.tgz", + "integrity": "sha512-FvuyNsY2oN8f2dvCl4Hx8CxBWCIF3BC9JE3Ay1lCuVqY1WYkvW4AQn3/0WVRINCxB9FkQxVNkSjARdwHNCEulw==", "license": "MIT" }, "node_modules/@patternfly/react-table": { @@ -4452,9 +4539,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.2.2.tgz", - "integrity": "sha512-2GRWDPBTrcTlGNFc5NPJjrjEVU90RpgcGX/CIe2MplLgM32tpVIkeUtqIoJPLRk5GrbhyFuHJYRU+O93gU4o3Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.3.0.tgz", + "integrity": "sha512-yWStfkbxg4RWAExFKS/JRGScyadOy35yr4DFispNeHrkZWMp4pwKf0VdwlQZ7+ZtSgEWtzzy1KFxMLmWh3mEqA==", "license": "MIT" }, "node_modules/@pkgjs/parseargs": { @@ -20520,6 +20607,12 @@ "dev": true, "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index fa70250a..567b22d5 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "whatwg-fetch": "^3.6.20" }, "dependencies": { + "@patternfly/react-drag-drop": "^6.3.0", "@patternfly/react-tokens": "^6.0.0", "sharp": "^0.33.5" } From e7abf62a16c56812dbbcf1d30ec99ffc2eb5a14d Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Mon, 4 Aug 2025 14:38:05 -0400 Subject: [PATCH 08/10] style: appease linter --- .../ColumnManagement/ColumnManagementExample.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx index 16e02420..53a71aee 100644 --- a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx @@ -39,17 +39,17 @@ export const ColumnExample: FunctionComponent = () => { columns={columns} onOrderChange={setColumns} onSelect={(col) => { - const newColumns = [...columns]; - const changedColumn = newColumns.find(c => c.key === col.key); - if (changedColumn) { - changedColumn.isShown = col.isShown; - } - setColumns(newColumns); + const newColumns = [ ...columns ]; + const changedColumn = newColumns.find(c => c.key === col.key); + if (changedColumn) { + changedColumn.isShown = col.isShown; + } + setColumns(newColumns); }} onSelectAll={(newColumns) => setColumns(newColumns)} onSave={(newColumns) => { - setColumns(newColumns); - alert('Changes saved!'); + setColumns(newColumns); + alert('Changes saved!'); }} onCancel={() => alert('Changes cancelled!')} /> From e510e5822d2610568626ad6c24e4e3a9493c8bbd Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Thu, 7 Aug 2025 13:38:43 -0400 Subject: [PATCH 09/10] feat: rename ColumnManagement to ListManager --- cypress/component/ListManager.cy.tsx | 70 +++++++++++++++++++ .../ListManager.md} | 12 ++-- .../ListManagerExample.tsx} | 6 +- packages/module/src/ColumnManagement/index.ts | 2 - .../ListManager.test.tsx} | 22 +++--- .../ListManager.tsx} | 17 ++--- packages/module/src/ListManager/index.ts | 2 + packages/module/src/index.ts | 6 +- 8 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 cypress/component/ListManager.cy.tsx rename packages/module/patternfly-docs/content/extensions/component-groups/examples/{ColumnManagement/ColumnManagement.md => ListManager/ListManager.md} (68%) rename packages/module/patternfly-docs/content/extensions/component-groups/examples/{ColumnManagement/ColumnManagementExample.tsx => ListManager/ListManagerExample.tsx} (85%) delete mode 100644 packages/module/src/ColumnManagement/index.ts rename packages/module/src/{ColumnManagement/ColumnManagement.test.tsx => ListManager/ListManager.test.tsx} (79%) rename packages/module/src/{ColumnManagement/ColumnManagement.tsx => ListManager/ListManager.tsx} (92%) create mode 100644 packages/module/src/ListManager/index.ts diff --git a/cypress/component/ListManager.cy.tsx b/cypress/component/ListManager.cy.tsx new file mode 100644 index 00000000..d9f0c023 --- /dev/null +++ b/cypress/component/ListManager.cy.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import ListManager, { Column, ListManagerProps } from '../../packages/module/dist/dynamic/ListManager'; + +const ALL_COLUMNS: Column[] = [ + { key: 'first', title: 'First', isShownByDefault: true }, + { key: 'second', title: 'Second', isShownByDefault: true }, + { key: 'third', title: 'Third', isShownByDefault: false }, + { key: 'fourth', title: 'Fourth', isShownByDefault: true, isUntoggleable: true }, +]; + +const ListManagerTest = (props: Partial) => { + const [columns, setColumns] = useState(props.columns || ALL_COLUMNS); + return setColumns(props.columns || ALL_COLUMNS)} />; +}; + +describe('ListManager', () => { + it('renders', () => { + cy.mount(); + cy.get('[data-ouia-component-id="Column-save-button"]').should('exist'); + cy.get('[data-ouia-component-id="Column-cancel-button"]').should('exist'); + cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist'); + cy.get('[data-ouia-component-id="Column-column-list"]').should('exist'); + }); + + it('toggles checkboxes', () => { + cy.mount(); + cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-second"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-third"]').find('input').should('not.be.checked'); + cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-fourth"]').find('input').should('be.disabled'); + + cy.get('[data-testid="column-check-first"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); + + cy.get('[data-testid="column-check-third"]').click(); + cy.get('[data-testid="column-check-third"]').find('input').should('be.checked'); + }); + + it('selects all and none', () => { + cy.mount(); + cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click(); + cy.get('[data-ouia-component-id="BulkSelect-select-none"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); + cy.get('[data-testid="column-check-second"]').find('input').should('not.be.checked'); + cy.get('[data-testid="column-check-third"]').find('input').should('not.be.checked'); + // fourth is untoggleable + cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); + + cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click(); + cy.get('[data-ouia-component-id="BulkSelect-select-all"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-second"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-third"]').find('input').should('be.checked'); + cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); + }); + + it('saves and cancels', () => { + cy.mount(); + cy.get('[data-testid="column-check-first"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); + cy.get('[data-ouia-component-id="Column-save-button"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); + + cy.get('[data-testid="column-check-first"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); + cy.get('[data-ouia-component-id="Column-cancel-button"]').click(); + cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); + }); +}); diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManager.md similarity index 68% rename from packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md rename to packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManager.md index 40de02c8..8a8ab9fa 100644 --- a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManager.md @@ -5,19 +5,19 @@ section: Component groups subsection: Helpers # Sidenav secondary level section # should be the same for all markdown files -id: Column management +id: List manager # 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: ['ColumnManagement'] -sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagement.md +propComponents: ['ListManager'] +sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManager.md --- -import ColumnManagement from '@patternfly/react-component-groups/dist/dynamic/ColumnManagement'; +import ListManager from '@patternfly/react-component-groups/dist/dynamic/ListManager'; import { FunctionComponent, useState } from 'react'; -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. +The **list manager** component can be used to implement customizable table columns. Columns can be configured to be enabled or disabled by default or be unhidable. ## Examples @@ -25,4 +25,4 @@ The **column management** component can be used to implement customizable table 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. -```js file="./ColumnManagementExample.tsx" +```js file="./ListManagerExample.tsx" diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManagerExample.tsx similarity index 85% rename from packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx rename to packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManagerExample.tsx index 53a71aee..6feb0094 100644 --- a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ColumnManagement/ColumnManagementExample.tsx +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ListManager/ListManagerExample.tsx @@ -1,5 +1,5 @@ import { FunctionComponent, useState } from 'react'; -import { Column, ColumnManagement } from '@patternfly/react-component-groups'; +import { Column, ListManager } from '@patternfly/react-component-groups'; const DEFAULT_COLUMNS: Column[] = [ { @@ -33,9 +33,7 @@ export const ColumnExample: FunctionComponent = () => { const [ columns, setColumns ] = useState(DEFAULT_COLUMNS); return ( - { diff --git a/packages/module/src/ColumnManagement/index.ts b/packages/module/src/ColumnManagement/index.ts deleted file mode 100644 index 283ecbe7..00000000 --- a/packages/module/src/ColumnManagement/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ColumnManagement'; -export * from './ColumnManagement'; diff --git a/packages/module/src/ColumnManagement/ColumnManagement.test.tsx b/packages/module/src/ListManager/ListManager.test.tsx similarity index 79% rename from packages/module/src/ColumnManagement/ColumnManagement.test.tsx rename to packages/module/src/ListManager/ListManager.test.tsx index 9cb4fa7c..7d515884 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.test.tsx +++ b/packages/module/src/ListManager/ListManager.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; -import ColumnManagement from './ColumnManagement'; +import ListManager from './ListManager'; jest.mock('@patternfly/react-drag-drop', () => { const originalModule = jest.requireActual('@patternfly/react-drag-drop'); @@ -23,23 +23,17 @@ const mockColumns = [ { key: 'version', title: 'Version', isShown: false, isShownByDefault: false }, ]; -describe('Column', () => { +describe('ListManager', () => { it('renders with initial columns', () => { - render(); + render(); expect(screen.getByTestId('column-check-name')).toBeChecked(); expect(screen.getByTestId('column-check-status')).toBeChecked(); expect(screen.getByTestId('column-check-version')).not.toBeChecked(); }); - it('renders title and description', () => { - render(); - expect(screen.getByText('Test Title')).toBeInTheDocument(); - expect(screen.getByText('Test Description')).toBeInTheDocument(); - }); - it('renders a cancel button', async () => { const onCancel = jest.fn(); - render(); + render(); const cancelButton = screen.getByText('Cancel'); expect(cancelButton).toBeInTheDocument(); await userEvent.click(cancelButton); @@ -48,7 +42,7 @@ describe('Column', () => { it('toggles a column', async () => { const onSelect = jest.fn(); - render(); + render(); const nameCheckbox = screen.getByTestId('column-check-name'); await userEvent.click(nameCheckbox); expect(nameCheckbox).not.toBeChecked(); @@ -56,7 +50,7 @@ describe('Column', () => { }); it('selects all columns', async () => { - render(); + render(); const menuToggle = screen.getByLabelText('Bulk select toggle'); if (menuToggle) { await userEvent.click(menuToggle); @@ -69,7 +63,7 @@ describe('Column', () => { }); it('selects no columns', async () => { - render(); + render(); const menuToggle = screen.getByLabelText('Bulk select toggle'); if (menuToggle) { await userEvent.click(menuToggle); @@ -83,7 +77,7 @@ describe('Column', () => { it('saves changes', async () => { const onSave = jest.fn(); - render(); + render(); const saveButton = screen.getByText('Save'); await userEvent.click(saveButton); expect(onSave).toHaveBeenCalledWith(expect.any(Array)); diff --git a/packages/module/src/ColumnManagement/ColumnManagement.tsx b/packages/module/src/ListManager/ListManager.tsx similarity index 92% rename from packages/module/src/ColumnManagement/ColumnManagement.tsx rename to packages/module/src/ListManager/ListManager.tsx index 13b79072..9a0df54a 100644 --- a/packages/module/src/ColumnManagement/ColumnManagement.tsx +++ b/packages/module/src/ListManager/ListManager.tsx @@ -8,7 +8,6 @@ import { DataListItemCells, Button, ButtonVariant, - Title } from '@patternfly/react-core'; import { DragDropSort, Droppable } from '@patternfly/react-drag-drop'; import BulkSelect, { BulkSelectValue } from '../BulkSelect'; @@ -26,13 +25,9 @@ export interface Column { isUntoggleable?: boolean; } -export interface ColumnProps { +export interface ListManagerProps { /** Current column state */ columns: Column[]; - /* Column description text */ - description?: string; - /* Column title text */ - title?: string; /** Custom OUIA ID */ ouiaId?: string | number; /** Callback when a column is selected or deselected */ @@ -47,16 +42,14 @@ export interface ColumnProps { onCancel?: () => void; } -const ColumnManagement: FunctionComponent = ( +const ListManager: FunctionComponent = ( { columns, - description, - title, ouiaId = 'Column', onSelect, onSelectAll, onOrderChange, onSave, - onCancel }: ColumnProps) => { + onCancel }: ListManagerProps) => { const [ currentColumns, setCurrentColumns ] = useState( () => columns.map(column => ({ ...column, isShown: column.isShown ?? column.isShownByDefault, id: column.key })) @@ -100,8 +93,6 @@ const ColumnManagement: FunctionComponent = ( return ( <> - {title} - {description &&

{description}

}
= ( ); } -export default ColumnManagement; +export default ListManager; diff --git a/packages/module/src/ListManager/index.ts b/packages/module/src/ListManager/index.ts new file mode 100644 index 00000000..c584beb0 --- /dev/null +++ b/packages/module/src/ListManager/index.ts @@ -0,0 +1,2 @@ +export { default } from './ListManager'; +export * from './ListManager'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 4f4c8193..82af8858 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -60,6 +60,9 @@ export * from './Maintenance'; export { default as LogSnippet } from './LogSnippet'; export * from './LogSnippet'; +export { default as ListManager } from './ListManager'; +export * from './ListManager'; + export { default as ExternalLinkButton } from './ExternalLinkButton'; export * from './ExternalLinkButton'; @@ -75,9 +78,6 @@ export * from './ErrorBoundary'; export { default as ColumnManagementModal } from './ColumnManagementModal'; export * from './ColumnManagementModal'; -export { default as ColumnManagement } from './ColumnManagement'; -export * from './ColumnManagement'; - export { default as CloseButton } from './CloseButton'; export * from './CloseButton'; From 8cf4db66c9c32fa79b8472954cc9bb71e2a77f89 Mon Sep 17 00:00:00 2001 From: Austin Pinkerton Date: Thu, 7 Aug 2025 13:42:25 -0400 Subject: [PATCH 10/10] test: remove unused listmanager cypress test --- cypress/component/ListManager.cy.tsx | 70 ---------------------------- 1 file changed, 70 deletions(-) delete mode 100644 cypress/component/ListManager.cy.tsx diff --git a/cypress/component/ListManager.cy.tsx b/cypress/component/ListManager.cy.tsx deleted file mode 100644 index d9f0c023..00000000 --- a/cypress/component/ListManager.cy.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useState } from 'react'; -import ListManager, { Column, ListManagerProps } from '../../packages/module/dist/dynamic/ListManager'; - -const ALL_COLUMNS: Column[] = [ - { key: 'first', title: 'First', isShownByDefault: true }, - { key: 'second', title: 'Second', isShownByDefault: true }, - { key: 'third', title: 'Third', isShownByDefault: false }, - { key: 'fourth', title: 'Fourth', isShownByDefault: true, isUntoggleable: true }, -]; - -const ListManagerTest = (props: Partial) => { - const [columns, setColumns] = useState(props.columns || ALL_COLUMNS); - return setColumns(props.columns || ALL_COLUMNS)} />; -}; - -describe('ListManager', () => { - it('renders', () => { - cy.mount(); - cy.get('[data-ouia-component-id="Column-save-button"]').should('exist'); - cy.get('[data-ouia-component-id="Column-cancel-button"]').should('exist'); - cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist'); - cy.get('[data-ouia-component-id="Column-column-list"]').should('exist'); - }); - - it('toggles checkboxes', () => { - cy.mount(); - cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-second"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-third"]').find('input').should('not.be.checked'); - cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-fourth"]').find('input').should('be.disabled'); - - cy.get('[data-testid="column-check-first"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); - - cy.get('[data-testid="column-check-third"]').click(); - cy.get('[data-testid="column-check-third"]').find('input').should('be.checked'); - }); - - it('selects all and none', () => { - cy.mount(); - cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click(); - cy.get('[data-ouia-component-id="BulkSelect-select-none"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); - cy.get('[data-testid="column-check-second"]').find('input').should('not.be.checked'); - cy.get('[data-testid="column-check-third"]').find('input').should('not.be.checked'); - // fourth is untoggleable - cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); - - cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click(); - cy.get('[data-ouia-component-id="BulkSelect-select-all"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-second"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-third"]').find('input').should('be.checked'); - cy.get('[data-testid="column-check-fourth"]').find('input').should('be.checked'); - }); - - it('saves and cancels', () => { - cy.mount(); - cy.get('[data-testid="column-check-first"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); - cy.get('[data-ouia-component-id="Column-save-button"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); - - cy.get('[data-testid="column-check-first"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('be.checked'); - cy.get('[data-ouia-component-id="Column-cancel-button"]').click(); - cy.get('[data-testid="column-check-first"]').find('input').should('not.be.checked'); - }); -});