Skip to content

Commit 5d3ec4c

Browse files
committed
feat: compose expandable
1 parent 1645dfa commit 5d3ec4c

File tree

5 files changed

+225
-34
lines changed

5 files changed

+225
-34
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { FunctionComponent, ReactNode } from 'react';
2+
import { DataViewTable, DataViewTr, DataViewTh, ExpandableContent } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
3+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
4+
import { Button } from '@patternfly/react-core';
5+
import { ActionsColumn, ExpandableRowContent } from '@patternfly/react-table';
6+
7+
interface Repository {
8+
id: number;
9+
name: string;
10+
branches: string | null;
11+
prs: string | null;
12+
workspaces: string;
13+
lastCommit: string;
14+
}
15+
16+
const expandableContents: ExpandableContent[] = [
17+
// Row 1 - Repository one
18+
{ row_id: 1, column_id: 2, content: <div><strong>Branch Details:</strong> 5 active branches, main, develop, feature/new-ui, hotfix/bug-123, release/v2.0</div> },
19+
{ row_id: 1, column_id: 3, content: <div><strong>PR Details:</strong> 3 open PRs, 45 merged this month, avg review time: 2 days</div> },
20+
{ row_id: 1, column_id: 4, content: <div><strong>Workspace Info:</strong> Production env, 5 active deployments, last updated 2 hours ago</div> },
21+
{ row_id: 1, column_id: 5, content: <div><strong>Commit Info:</strong> Author: John Doe, Message: "Fix critical authentication bug", SHA: a1b2c3d</div> },
22+
23+
// Row 2 - Repository two
24+
{ row_id: 2, column_id: 0, content: <div><strong>Favorites Details:</strong> Recently added</div> },
25+
{ row_id: 2, column_id: 1, content: <div><strong>Repository Details:</strong> Created 3 months ago, 45 contributors, Apache 2.0 License</div> },
26+
{ row_id: 2, column_id: 2, content: <div><strong>Branch Details:</strong> 8 active branches, main, staging, feature/api-v2, feature/dashboard</div> },
27+
{ row_id: 2, column_id: 3, content: <div><strong>PR Details:</strong> 5 open PRs, 120 merged this month, avg review time: 1.5 days</div> },
28+
{ row_id: 2, column_id: 4, content: <div><strong>Workspace Info:</strong> Development env, 3 active deployments, last updated 30 mins ago</div> },
29+
{ row_id: 2, column_id: 5, content: <div><strong>Commit Info:</strong> Author: Jane Smith, Message: "Add new API endpoints", SHA: x9y8z7w</div> },
30+
31+
// Row 3 - Repository three
32+
{ row_id: 3, column_id: 0, content: <div><strong>Favorites Details:</strong> Top starred repository</div> },
33+
{ row_id: 3, column_id: 1, content: <div><strong>Repository Details:</strong> Created 1 year ago, 200 contributors, GPL v3 License</div> },
34+
{ row_id: 3, column_id: 2, content: <div><strong>Branch Details:</strong> 12 active branches including main, develop, multiple feature branches</div> },
35+
{ row_id: 3, column_id: 3, content: <div><strong>PR Details:</strong> 8 open PRs, 200 merged this month, avg review time: 3 days</div> },
36+
{ row_id: 3, column_id: 4, content: <div><strong>Workspace Info:</strong> Staging env, 10 active deployments, last updated 1 day ago</div> },
37+
{ row_id: 3, column_id: 5, content: <div><strong>Commit Info:</strong> Author: Bob Johnson, Message: "Refactor core modules", SHA: p0o9i8u</div> },
38+
39+
// Row 4 - Repository four
40+
{ row_id: 4, column_id: 0, content: <div><strong>Favorites Details:</strong> Added 2 weeks ago</div> },
41+
{ row_id: 4, column_id: 1, content: <div><strong>Repository Details:</strong> Created 2 years ago, 80 contributors, BSD License</div> },
42+
{ row_id: 4, column_id: 2, content: <div><strong>Branch Details:</strong> 6 active branches, focusing on microservices architecture</div> },
43+
{ row_id: 4, column_id: 3, content: <div><strong>PR Details:</strong> 2 open PRs, 90 merged this month, avg review time: 2.5 days</div> },
44+
{ row_id: 4, column_id: 4, content: <div><strong>Workspace Info:</strong> QA env, 7 active deployments, automated testing enabled</div> },
45+
{ row_id: 4, column_id: 5, content: <div><strong>Commit Info:</strong> Author: Alice Williams, Message: "Update dependencies", SHA: m5n4b3v</div> },
46+
47+
// Row 5 - Repository five
48+
{ row_id: 5, column_id: 0, content: <div><strong>Favorites Details:</strong> Most viewed this week</div> },
49+
{ row_id: 5, column_id: 1, content: <div><strong>Repository Details:</strong> Created 8 months ago, 60 contributors, ISC License</div> },
50+
{ row_id: 5, column_id: 2, content: <div><strong>Branch Details:</strong> 4 active branches, clean branch strategy</div> },
51+
{ row_id: 5, column_id: 3, content: <div><strong>PR Details:</strong> 6 open PRs, 75 merged this month, avg review time: 1 day</div> },
52+
{ row_id: 5, column_id: 4, content: <div><strong>Workspace Info:</strong> Pre-production env, CI/CD pipeline configured</div> },
53+
{ row_id: 5, column_id: 5, content: <div><strong>Commit Info:</strong> Author: Charlie Brown, Message: "Implement dark mode", SHA: q2w3e4r</div> },
54+
55+
// Row 6 - Repository six
56+
{ row_id: 6, column_id: 0, content: <div><strong>Favorites Details:</strong> Legacy favorite</div> },
57+
{ row_id: 6, column_id: 1, content: <div><strong>Repository Details:</strong> Created 5 years ago, 300 contributors, MIT License</div> },
58+
{ row_id: 6, column_id: 2, content: <div><strong>Branch Details:</strong> 15 active branches, complex branching model</div> },
59+
{ row_id: 6, column_id: 3, content: <div><strong>PR Details:</strong> 10 open PRs, 250 merged this month, avg review time: 4 days</div> },
60+
{ row_id: 6, column_id: 4, content: <div><strong>Workspace Info:</strong> Multi-region deployment, high availability setup</div> },
61+
{ row_id: 6, column_id: 5, content: <div><strong>Commit Info:</strong> Author: David Lee, Message: "Security patches applied", SHA: t6y7u8i</div> },
62+
];
63+
64+
const repositories: Repository[] = [
65+
{ id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one'},
66+
{ id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two'},
67+
{ id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three'},
68+
{ id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four'},
69+
{ id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five'},
70+
{ id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six'}
71+
];
72+
73+
const rowActions = [
74+
{
75+
title: 'Some action',
76+
onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console
77+
},
78+
{
79+
title: <div>Another action</div>,
80+
onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console
81+
},
82+
{
83+
isSeparator: true
84+
},
85+
{
86+
title: 'Third action',
87+
onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console
88+
}
89+
];
90+
91+
// you can also pass props to Tr by returning { row: DataViewTd[], props: TrProps } }
92+
const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [
93+
{
94+
id,
95+
cell: workspaces,
96+
props: {
97+
favorites: { isFavorited: true }
98+
}
99+
},
100+
{ cell: <Button href='#' variant='link' isInline>{name}</Button> },
101+
branches,
102+
prs,
103+
workspaces,
104+
lastCommit,
105+
{ cell: <ActionsColumn items={rowActions}/>, props: { isActionCell: true } },
106+
]);
107+
108+
const columns: DataViewTh[] = [
109+
null,
110+
'Repositories',
111+
{ cell: <>Branches<ExclamationCircleIcon className='pf-v6-u-ml-sm' color="var(--pf-t--global--color--status--danger--default)"/></> },
112+
'Pull requests',
113+
{ cell: 'Workspaces', props: { info: { tooltip: 'More information' } } },
114+
{ cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 } } },
115+
];
116+
117+
const ouiaId = 'TableExample';
118+
119+
export const BasicExample: FunctionComponent = () => (
120+
<DataViewTable aria-label='Repositories table' ouiaId={ouiaId} columns={columns} rows={rows} expandedRows={expandableContents}/>
121+
);

packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ If you want to have all expandable nodes open on initial load pass the `expandAl
5858

5959
```
6060

61+
### Table example
62+
63+
```js file="./DataViewTableExpandableExample.tsx"
64+
65+
```
66+
6167
### Resizable columns
6268

6369
To allow a column to resize, add `isResizable` to the `DataViewTable` element, and pass `resizableProps` to each applicable header cell. The `resizableProps` object consists of the following fields:

packages/module/patternfly-docs/generated/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ module.exports = {
1414
'/extensions/data-view/table/react': {
1515
id: "Table",
1616
title: "Data view table",
17-
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"},{"text":"Resizable columns"}],{"text":"Tree table"},[{"text":"Tree table example"}],{"text":"Sorting"},[{"text":"Sorting example"},{"text":"Sorting state"}],{"text":"States"},[{"text":"Empty"},{"text":"Error"},{"text":"Loading"}]],
18-
examples: ["Table example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
17+
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"},{"text":"Table example"},{"text":"Resizable columns"}],{"text":"Tree table"},[{"text":"Tree table example"}],{"text":"Sorting"},[{"text":"Sorting example"},{"text":"Sorting state"}],{"text":"States"},[{"text":"Empty"},{"text":"Error"},{"text":"Loading"}]],
18+
examples: ["Table example","Table example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
1919
section: "extensions",
2020
subsection: "Data view",
2121
source: "react",
@@ -26,7 +26,7 @@ module.exports = {
2626
'/extensions/data-view/overview/extensions': {
2727
id: "Overview",
2828
title: "Data view overview",
29-
toc: [[{"text":"Layout"},{"text":"Modularity"}],{"text":"Events context"},[{"text":"Row click subscription example"}]],
29+
toc: [{"text":"How to structure and implement the data view"},[{"text":"Layout"},{"text":"Modularity"}],{"text":"Events context"},[{"text":"Row click subscription example"}]],
3030
examples: ["Layout","Modularity","Row click subscription example"],
3131
section: "extensions",
3232
subsection: "Data view",

packages/module/src/DataViewTable/DataViewTable.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { FC, ReactNode } from 'react';
22
import { TdProps, ThProps, TrProps, InnerScrollContainer } from '@patternfly/react-table';
33
import { DataViewTableTree, DataViewTableTreeProps } from '../DataViewTableTree';
4-
import { DataViewTableBasic, DataViewTableBasicProps } from '../DataViewTableBasic';
4+
import { DataViewTableBasic, DataViewTableBasicProps, ExpandableContent } from '../DataViewTableBasic';
55
import { DataViewThResizableProps } from '../DataViewTh/DataViewTh';
66

7+
// Re-export ExpandableContent for convenience
8+
export type { ExpandableContent };
9+
710
// Table head typings
811
export type DataViewTh =
912
| ReactNode

packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { FC, useMemo } from 'react';
1+
import { FC, useMemo, useState } from 'react';
22
import {
3+
expandable,
4+
ExpandableRowContent,
35
Table,
46
TableProps,
57
Tbody,
@@ -8,15 +10,23 @@ import {
810
} from '@patternfly/react-table';
911
import { useInternalContext } from '../InternalContext';
1012
import { DataViewTableHead } from '../DataViewTableHead';
11-
import { DataViewTh, DataViewTr, isDataViewTdObject, isDataViewTrObject } from '../DataViewTable';
13+
import { DataViewTh, DataViewTr, isDataViewTdObject, isDataViewThObject, isDataViewTrObject } from '../DataViewTable';
1214
import { DataViewState } from '../DataView/DataView';
1315

16+
export interface ExpandableContent {
17+
row_id: number;
18+
column_id: number;
19+
content: React.ReactNode;
20+
}
21+
1422
/** extends TableProps */
1523
export interface DataViewTableBasicProps extends Omit<TableProps, 'onSelect' | 'rows'> {
1624
/** Columns definition */
1725
columns: DataViewTh[];
1826
/** Current page rows */
1927
rows: DataViewTr[];
28+
/** Expanded rows content */
29+
expandedRows?: ExpandableContent[];
2030
/** Table head states to be displayed when active */
2131
headStates?: Partial<Record<DataViewState | string, React.ReactNode>>
2232
/** Table body states to be displayed when active */
@@ -25,15 +35,19 @@ export interface DataViewTableBasicProps extends Omit<TableProps, 'onSelect' | '
2535
ouiaId?: string;
2636
/** @hide Indicates if the table is resizable */
2737
hasResizableColumns?: boolean;
38+
/** Toggles expandable */
39+
isExpandable?: boolean;
2840
}
2941

3042
export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
3143
columns,
3244
rows,
45+
expandedRows,
3346
ouiaId = 'DataViewTableBasic',
3447
headStates,
3548
bodyStates,
3649
hasResizableColumns,
50+
isExpandable,
3751
...props
3852
}: DataViewTableBasicProps) => {
3953
const { selection, activeState, isSelectable } = useInternalContext();
@@ -42,43 +56,90 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
4256
const activeHeadState = useMemo(() => activeState ? headStates?.[activeState] : undefined, [ activeState, headStates ]);
4357
const activeBodyState = useMemo(() => activeState ? bodyStates?.[activeState] : undefined, [ activeState, bodyStates ]);
4458

59+
const [expandedRowsState, setExpandedRowsState] = useState<Record<number, boolean>>({})
60+
const [expandedColumnIndex, setExpandedColumnIndex] = useState<Record<number, number>>({})
61+
4562
const renderedRows = useMemo(() => rows.map((row, rowIndex) => {
4663
const rowIsObject = isDataViewTrObject(row);
64+
const isRowExpanded = expandedRowsState[rowIndex] || false;
65+
const expandedColIndex = expandedColumnIndex[rowIndex];
66+
67+
// Get the first cell to extract the row ID
68+
const rowData = rowIsObject ? row.row : row;
69+
const firstCell = rowData[0];
70+
const rowId = isDataViewTdObject(firstCell) ? (firstCell as any).id : undefined;
71+
72+
// Find the matching expanded content by row_id and column_id
73+
const expandedRowContent = expandedRows?.find(
74+
(content) => content.row_id === rowId && content.column_id === expandedColIndex
75+
);
76+
4777
return (
48-
<Tr key={rowIndex} ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)}>
49-
{isSelectable && (
50-
<Td
51-
key={`select-${rowIndex}`}
52-
select={{
53-
rowIndex,
54-
onSelect: (_event, isSelecting) => {
55-
onSelect?.(isSelecting, rowIsObject ? row : [ row ]);
56-
},
57-
isSelected: isSelected?.(row) || false,
58-
isDisabled: isSelectDisabled?.(row) || false,
59-
}}
60-
/>
61-
)}
62-
{(rowIsObject ? row.row : row).map((cell, colIndex) => {
63-
const cellIsObject = isDataViewTdObject(cell);
64-
return (
78+
<Tbody key={rowIndex} isExpanded={isRowExpanded}>
79+
<Tr ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)} isContentExpanded={isRowExpanded} isControlRow>
80+
{isSelectable && (
6581
<Td
66-
key={colIndex}
67-
{...(cellIsObject && (cell?.props ?? {}))}
68-
data-ouia-component-id={`${ouiaId}-td-${rowIndex}-${colIndex}`}
69-
>
70-
{cellIsObject ? cell.cell : cell}
82+
key={`select-${rowIndex}`}
83+
select={{
84+
rowIndex,
85+
onSelect: (_event, isSelecting) => {
86+
onSelect?.(isSelecting, rowIsObject ? row : [ row ]);
87+
},
88+
isSelected: isSelected?.(row) || false,
89+
isDisabled: isSelectDisabled?.(row) || false,
90+
}}
91+
/>
92+
)}
93+
{(rowIsObject ? row.row : row).map((cell, colIndex) => {
94+
const cellIsObject = isDataViewTdObject(cell);
95+
const cellExpandableContent = expandedRows?.find(
96+
(content) => content.row_id === rowId && content.column_id === colIndex
97+
);
98+
return (
99+
<Td
100+
key={colIndex}
101+
{...(cellIsObject && (cell?.props ?? {}))}
102+
{...(cellExpandableContent != null && {
103+
compoundExpand: {
104+
isExpanded: isRowExpanded && expandedColIndex === colIndex,
105+
expandId: `expandable-${rowIndex}`,
106+
onToggle: () => {
107+
console.log(`toggled compound expand for row ${rowIndex}, column ${colIndex}`); // eslint-disable-line no-console
108+
setExpandedRowsState(prev => {
109+
const isSameColumn = expandedColIndex === colIndex;
110+
const wasExpanded = prev[rowIndex];
111+
return { ...prev, [rowIndex]: isSameColumn ? !wasExpanded : true };
112+
});
113+
setExpandedColumnIndex(prev => ({ ...prev, [rowIndex]: colIndex }));
114+
},
115+
rowIndex,
116+
columnIndex: colIndex
117+
}
118+
})}
119+
data-ouia-component-id={`${ouiaId}-td-${rowIndex}-${colIndex}`}
120+
>
121+
{cellIsObject ? cell.cell : cell}
122+
</Td>
123+
);
124+
})}
125+
</Tr>
126+
{expandedRowContent && (
127+
<Tr isExpanded={isRowExpanded}>
128+
<Td colSpan={rowData.length + (isSelectable ? 1 : 0)} data-expanded-column-index={expandedColIndex}>
129+
<ExpandableRowContent>
130+
{expandedRowContent.content}
131+
</ExpandableRowContent>
71132
</Td>
72-
);
73-
})}
74-
</Tr>
133+
</Tr>
134+
)}
135+
</Tbody>
75136
);
76-
}), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId ]);
137+
}), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId, expandedRowsState, expandedColumnIndex, expandedRows, columns.length ]);
77138

78139
return (
79-
<Table aria-label="Data table" ouiaId={ouiaId} {...props}>
140+
<Table aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} {...props}>
80141
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
81-
{ activeBodyState || <Tbody>{renderedRows}</Tbody> }
142+
{ activeBodyState || renderedRows }
82143
</Table>
83144
);
84145
};

0 commit comments

Comments
 (0)