diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableDraggableExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableDraggableExample.tsx new file mode 100644 index 00000000..6d817ad8 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableDraggableExample.tsx @@ -0,0 +1,72 @@ +import { FunctionComponent } from 'react'; +import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; +import { ActionsColumn } from '@patternfly/react-table'; + +interface Repository { + id: number; + name: string; + branches: string | null; + prs: string | null; + workspaces: string; + lastCommit: string; +} + +const repositories: Repository[] = [ + { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' }, + { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' }, + { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' }, + { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' }, + { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' }, + { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' } +]; + +const rowActions = [ + { + title: 'Some action', + onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console + }, + { + title:
Another action
, + onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console + }, + { + isSeparator: true + }, + { + title: 'Third action', + onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console + } +]; + +const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [ + { id, cell: workspaces, props: { favorites: { isFavorited: true } } }, + { cell: }, + branches, + prs, + workspaces, + lastCommit, + { cell: , props: { isActionCell: true } }, +]); + +const columns: DataViewTh[] = [ + null, + 'Repositories', + { cell: <>Branches }, + 'Pull requests', + { cell: 'Workspaces', props: { info: { tooltip: 'More information' } } }, + { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 } } }, +]; + +const ouiaId = 'TableDraggableExample'; + +export const DraggableExample: FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExample.tsx index c293db0a..d7513d2f 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExample.tsx +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExample.tsx @@ -63,5 +63,5 @@ const columns: DataViewTh[] = [ const ouiaId = 'TableExample'; export const BasicExample: FunctionComponent = () => ( - + ); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx new file mode 100644 index 00000000..dc4c061b --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableExpandableExample.tsx @@ -0,0 +1,108 @@ +import { FunctionComponent } from 'react'; +import { DataViewTable, DataViewTr, DataViewTh, ExpandableContent } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; +import { ActionsColumn } from '@patternfly/react-table'; + +interface Repository { + id: number; + name: string; + branches: string | null; + prs: string | null; + workspaces: string; + lastCommit: string; +} + +const expandableContents: ExpandableContent[] = [ + // Row 1 - Repository one + { rowId: 1, columnId: 3, content:
PR Details: 3 open PRs, 45 merged this month, avg review time: 2 days
}, + { rowId: 1, columnId: 5, content:
Commit Info: Author: John Doe, Message: "Fix critical authentication bug", SHA: a1b2c3d
}, + + // Row 2 - Repository two + { rowId: 2, columnId: 2, content:
Branch Details: 8 active branches, main, staging, feature/api-v2, feature/dashboard
}, + { rowId: 2, columnId: 3, content:
PR Details: 5 open PRs, 120 merged this month, avg review time: 1.5 days
}, + { rowId: 2, columnId: 4, content:
Workspace Info: Development env, 3 active deployments, last updated 30 mins ago
}, + { rowId: 2, columnId: 5, content:
Commit Info: Author: Jane Smith, Message: "Add new API endpoints", SHA: x9y8z7w
}, + + // Row 3 - Repository three + { rowId: 3, columnId: 2, content:
Branch Details: 12 active branches including main, develop, multiple feature branches
}, + { rowId: 3, columnId: 3, content:
PR Details: 8 open PRs, 200 merged this month, avg review time: 3 days
}, + { rowId: 3, columnId: 4, content:
Workspace Info: Staging env, 10 active deployments, last updated 1 day ago
}, + { rowId: 3, columnId: 5, content:
Commit Info: Author: Bob Johnson, Message: "Refactor core modules", SHA: p0o9i8u
}, + + // Row 4 - Repository four + { rowId: 4, columnId: 2, content:
Branch Details: 6 active branches, focusing on microservices architecture
}, + { rowId: 4, columnId: 3, content:
PR Details: 2 open PRs, 90 merged this month, avg review time: 2.5 days
}, + { rowId: 4, columnId: 4, content:
Workspace Info: QA env, 7 active deployments, automated testing enabled
}, + { rowId: 4, columnId: 5, content:
Commit Info: Author: Alice Williams, Message: "Update dependencies", SHA: m5n4b3v
}, + + // Row 5 - Repository five + { rowId: 5, columnId: 2, content:
Branch Details: 4 active branches, clean branch strategy
}, + { rowId: 5, columnId: 3, content:
PR Details: 6 open PRs, 75 merged this month, avg review time: 1 day
}, + { rowId: 5, columnId: 4, content:
Workspace Info: Pre-production env, CI/CD pipeline configured
}, + { rowId: 5, columnId: 5, content:
Commit Info: Author: Charlie Brown, Message: "Implement dark mode", SHA: q2w3e4r
}, + + // Row 6 - Repository six + { rowId: 6, columnId: 2, content:
Branch Details: 15 active branches, complex branching model
}, + { rowId: 6, columnId: 3, content:
PR Details: 10 open PRs, 250 merged this month, avg review time: 4 days
}, + { rowId: 6, columnId: 4, content:
Workspace Info: Multi-region deployment, high availability setup
}, + { rowId: 6, columnId: 5, content:
Commit Info: Author: David Lee, Message: "Security patches applied", SHA: t6y7u8i
}, +]; + +const repositories: Repository[] = [ + { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' }, + { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' }, + { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' }, + { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' }, + { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' }, + { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' } +]; + +const rowActions = [ + { + title: 'Some action', + onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console + }, + { + title:
Another action
, + onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console + }, + { + isSeparator: true + }, + { + title: 'Third action', + onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console + } +]; + +const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [ + { + id, + cell: workspaces, + props: { + favorites: { isFavorited: true } + } + }, + { cell: }, + branches, + prs, + workspaces, + lastCommit, + { cell: , props: { isActionCell: true } }, +]); + +const columns: DataViewTh[] = [ + null, + 'Repositories', + { cell: <>Branches }, + 'Pull requests', + { cell: 'Workspaces', props: { info: { tooltip: 'More information' }, isStickyColumn: true } }, + { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 } } }, +]; + +const ouiaId = 'TableExample'; + +export const ExpandableExample: FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx new file mode 100644 index 00000000..0d810e73 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableInteractiveExample.tsx @@ -0,0 +1,158 @@ +import { FunctionComponent, useState } from 'react'; +import { DataViewTable, DataViewTr, DataViewTh, ExpandableContent } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { Button, Toolbar, ToolbarContent, ToolbarItem, Switch } from '@patternfly/react-core'; + +interface Repository { + id: number; + name: string; + branches: string | null; + prs: string | null; + workspaces: string; + lastCommit: string; + contributors: string; + stars: string; + forks: string; +} + +const expandableContents: ExpandableContent[] = [ + // Row 1 - Repository one + { rowId: 1, columnId: 2, content:
Branch Details: 5 active branches, main, develop, feature/new-ui, hotfix/bug-123, release/v2.0
}, + { rowId: 1, columnId: 3, content:
PR Details: 3 open PRs, 45 merged this month, avg review time: 2 days
}, + { rowId: 1, columnId: 5, content:
Commit Info: Author: John Doe, Message: "Fix critical authentication bug", SHA: a1b2c3d
}, + + // Row 2 - Repository two + { rowId: 2, columnId: 2, content:
Branch Details: 8 active branches, main, staging, feature/api-v2, feature/dashboard
}, + { rowId: 2, columnId: 3, content:
PR Details: 5 open PRs, 120 merged this month, avg review time: 1.5 days
}, + { rowId: 2, columnId: 4, content:
Workspace Info: Development env, 3 active deployments, last updated 30 mins ago
}, + { rowId: 2, columnId: 5, content:
Commit Info: Author: Jane Smith, Message: "Add new API endpoints", SHA: x9y8z7w
}, + + // Row 3 - Repository three + { rowId: 3, columnId: 2, content:
Branch Details: 12 active branches including main, develop, multiple feature branches
}, + { rowId: 3, columnId: 3, content:
PR Details: 8 open PRs, 200 merged this month, avg review time: 3 days
}, + { rowId: 3, columnId: 4, content:
Workspace Info: Staging env, 10 active deployments, last updated 1 day ago
}, + { rowId: 3, columnId: 5, content:
Commit Info: Author: Bob Johnson, Message: "Refactor core modules", SHA: p0o9i8u
}, + + // Row 4 - Repository four + { rowId: 4, columnId: 2, content:
Branch Details: 6 active branches, focusing on microservices architecture
}, + { rowId: 4, columnId: 3, content:
PR Details: 2 open PRs, 90 merged this month, avg review time: 2.5 days
}, + { rowId: 4, columnId: 4, content:
Workspace Info: QA env, 7 active deployments, automated testing enabled
}, + { rowId: 4, columnId: 5, content:
Commit Info: Author: Alice Williams, Message: "Update dependencies", SHA: m5n4b3v
}, + + // Row 5 - Repository five + { rowId: 5, columnId: 2, content:
Branch Details: 4 active branches, clean branch strategy
}, + { rowId: 5, columnId: 3, content:
PR Details: 6 open PRs, 75 merged this month, avg review time: 1 day
}, + { rowId: 5, columnId: 4, content:
Workspace Info: Pre-production env, CI/CD pipeline configured
}, + { rowId: 5, columnId: 5, content:
Commit Info: Author: Charlie Brown, Message: "Implement dark mode", SHA: q2w3e4r
}, + + // Row 6 - Repository six + { rowId: 6, columnId: 2, content:
Branch Details: 15 active branches, complex branching model
}, + { rowId: 6, columnId: 3, content:
PR Details: 10 open PRs, 250 merged this month, avg review time: 4 days
}, + { rowId: 6, columnId: 4, content:
Workspace Info: Multi-region deployment, high availability setup
}, + { rowId: 6, columnId: 5, content:
Commit Info: Author: David Lee, Message: "Security patches applied", SHA: t6y7u8i
}, +]; + +const repositories: Repository[] = [ + { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one', contributors: '25 contributors', stars: '1.2k stars', forks: '340 forks' }, + { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two', contributors: '45 contributors', stars: '3.5k stars', forks: '890 forks' }, + { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three', contributors: '200 contributors', stars: '15k stars', forks: '2.1k forks' }, + { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four', contributors: '80 contributors', stars: '5.7k stars', forks: '1.2k forks' }, + { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five', contributors: '60 contributors', stars: '4.3k stars', forks: '780 forks' }, + { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six', contributors: '300 contributors', stars: '22k stars', forks: '4.5k forks' }, + { id: 7, name: 'Repository seven', branches: 'Branch seven', prs: 'Pull request seven', workspaces: 'Workspace seven', lastCommit: 'Timestamp seven', contributors: '12 contributors', stars: '567 stars', forks: '120 forks' }, + { id: 8, name: 'Repository eight', branches: 'Branch eight', prs: 'Pull request eight', workspaces: 'Workspace eight', lastCommit: 'Timestamp eight', contributors: '98 contributors', stars: '7.8k stars', forks: '1.5k forks' }, + { id: 9, name: 'Repository nine', branches: 'Branch nine', prs: 'Pull request nine', workspaces: 'Workspace nine', lastCommit: 'Timestamp nine', contributors: '33 contributors', stars: '2.1k stars', forks: '456 forks' }, + { id: 10, name: 'Repository ten', branches: 'Branch ten', prs: 'Pull request ten', workspaces: 'Workspace ten', lastCommit: 'Timestamp ten', contributors: '150 contributors', stars: '11k stars', forks: '2.8k forks' }, + { id: 11, name: 'Repository eleven', branches: 'Branch eleven', prs: 'Pull request eleven', workspaces: 'Workspace eleven', lastCommit: 'Timestamp eleven', contributors: '67 contributors', stars: '5.2k stars', forks: '980 forks' }, + { id: 12, name: 'Repository twelve', branches: 'Branch twelve', prs: 'Pull request twelve', workspaces: 'Workspace twelve', lastCommit: 'Timestamp twelve', contributors: '41 contributors', stars: '3.1k stars', forks: '670 forks' }, + { id: 13, name: 'Repository thirteen', branches: 'Branch thirteen', prs: 'Pull request thirteen', workspaces: 'Workspace thirteen', lastCommit: 'Timestamp thirteen', contributors: '89 contributors', stars: '6.4k stars', forks: '1.3k forks' }, + { id: 14, name: 'Repository fourteen', branches: 'Branch fourteen', prs: 'Pull request fourteen', workspaces: 'Workspace fourteen', lastCommit: 'Timestamp fourteen', contributors: '120 contributors', stars: '9.2k stars', forks: '1.9k forks' }, + { id: 15, name: 'Repository fifteen', branches: 'Branch fifteen', prs: 'Pull request fifteen', workspaces: 'Workspace fifteen', lastCommit: 'Timestamp fifteen', contributors: '78 contributors', stars: '5.9k stars', forks: '1.1k forks' } +]; + +const ouiaId = 'TableInteractiveExample'; + +export const InteractiveExample: FunctionComponent = () => { + const [isExpandable, setIsExpandable] = useState(true); + const [isSticky, setIsSticky] = useState(true); + const [isDraggable, setIsDraggable] = useState(true); + + // Generate rows based on current settings + const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [ + { + id, + cell: workspaces, + props: { + favorites: { isFavorited: true } + } + }, + { cell: , props: { isStickyColumn: isSticky, hasRightBorder: true, hasLeftBorder: true, modifier: "nowrap" } }, + { cell: branches, props: { modifier: "nowrap" } }, + { cell: prs, props: { modifier: "nowrap" } }, + { cell: workspaces, props: { modifier: "nowrap" } }, + { cell: lastCommit, props: { modifier: "nowrap" } }, + { cell: contributors, props: { modifier: "nowrap" } }, + { cell: stars, props: { modifier: "nowrap" } }, + { cell: forks, props: { modifier: "nowrap" } } + ]); + + const columns: DataViewTh[] = [ + null, + { cell: 'Repositories', props: { isStickyColumn: isSticky, modifier: 'fitContent', hasRightBorder: true, hasLeftBorder: true } }, + { cell: <>Branches, props: { width: 20 } }, + { cell: 'Pull requests', props: { width: 20 } }, + { cell: 'Workspaces', props: { info: { tooltip: 'More information' }, width: 20 } }, + { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 }, width: 20 } }, + { cell: 'Contributors', props: { width: 20 } }, + { cell: 'Stars', props: { width: 20 } }, + { cell: 'Forks', props: { width: 20 } }, + ]; + + return ( + <> + + + + setIsExpandable(checked)} + aria-label="Toggle expandable rows" + /> + + + setIsSticky(checked)} + aria-label="Toggle sticky header and columns" + /> + + + setIsDraggable(checked)} + aria-label="Toggle draggable rows" + /> + + + +
+ +
+ + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickyExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickyExample.tsx new file mode 100644 index 00000000..ce6fa304 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickyExample.tsx @@ -0,0 +1,90 @@ +import { FunctionComponent } from 'react'; +import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { Button } from '@patternfly/react-core'; +import { ActionsColumn } from '@patternfly/react-table'; + +interface Repository { + id: number; + name: string; + branches: string | null; + prs: string | null; + workspaces: string; + lastCommit: string; + contributors: string; + stars: string; + forks: string; +} + +const repositories: Repository[] = [ + { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one', contributors: '25 contributors', stars: '1.2k stars', forks: '340 forks' }, + { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two', contributors: '45 contributors', stars: '3.5k stars', forks: '890 forks' }, + { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three', contributors: '200 contributors', stars: '15k stars', forks: '2.1k forks' }, + { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four', contributors: '80 contributors', stars: '5.7k stars', forks: '1.2k forks' }, + { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five', contributors: '60 contributors', stars: '4.3k stars', forks: '780 forks' }, + { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six', contributors: '300 contributors', stars: '22k stars', forks: '4.5k forks' }, + { id: 7, name: 'Repository seven', branches: 'Branch seven', prs: 'Pull request seven', workspaces: 'Workspace seven', lastCommit: 'Timestamp seven', contributors: '12 contributors', stars: '567 stars', forks: '120 forks' }, + { id: 8, name: 'Repository eight', branches: 'Branch eight', prs: 'Pull request eight', workspaces: 'Workspace eight', lastCommit: 'Timestamp eight', contributors: '98 contributors', stars: '7.8k stars', forks: '1.5k forks' }, + { id: 9, name: 'Repository nine', branches: 'Branch nine', prs: 'Pull request nine', workspaces: 'Workspace nine', lastCommit: 'Timestamp nine', contributors: '33 contributors', stars: '2.1k stars', forks: '456 forks' }, + { id: 10, name: 'Repository ten', branches: 'Branch ten', prs: 'Pull request ten', workspaces: 'Workspace ten', lastCommit: 'Timestamp ten', contributors: '150 contributors', stars: '11k stars', forks: '2.8k forks' }, + { id: 11, name: 'Repository eleven', branches: 'Branch eleven', prs: 'Pull request eleven', workspaces: 'Workspace eleven', lastCommit: 'Timestamp eleven', contributors: '67 contributors', stars: '5.2k stars', forks: '980 forks' }, + { id: 12, name: 'Repository twelve', branches: 'Branch twelve', prs: 'Pull request twelve', workspaces: 'Workspace twelve', lastCommit: 'Timestamp twelve', contributors: '41 contributors', stars: '3.1k stars', forks: '670 forks' } +]; + +const rowActions = [ + { + title: 'Some action', + onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console + }, + { + title:
Another action
, + onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console + }, + { + isSeparator: true + }, + { + title: 'Third action', + onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console + } +]; + +const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [ + { id, cell: workspaces, props: { favorites: { isFavorited: true } } }, + { cell: , props: { isStickyColumn: true, hasRightBorder: true, hasLeftBorder: true, modifier: "nowrap" } }, + { cell: branches, props: { modifier: "nowrap" } }, + { cell: prs, props: { modifier: "nowrap" } }, + { cell: workspaces, props: { modifier: "nowrap" } }, + { cell: lastCommit, props: { modifier: "nowrap" } }, + { cell: contributors, props: { modifier: "nowrap" } }, + { cell: stars, props: { modifier: "nowrap" } }, + { cell: forks, props: { modifier: "nowrap" } }, + { cell: , props: { isActionCell: true } }, +]); + +const columns: DataViewTh[] = [ + null, + { cell: 'Repositories', props: { isStickyColumn: true, modifier: 'fitContent', hasRightBorder: true, hasLeftBorder: true } }, + { cell: <>Branches, props: { width: 20 } }, + { cell: 'Pull requests', props: { width: 20 } }, + { cell: 'Workspaces', props: { info: { tooltip: 'More information' }, width: 20 } }, + { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 }, width: 20 } }, + { cell: 'Contributors', props: { width: 20 } }, + { cell: 'Stars', props: { width: 20 } }, + { cell: 'Forks', props: { width: 20 } }, + null, // Actions column header +]; + +const ouiaId = 'TableStickyExample'; + +export const StickyExample: FunctionComponent = () => ( +
+ +
+); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md index 23a63a83..35a3114d 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md @@ -58,6 +58,85 @@ If you want to have all expandable nodes open on initial load pass the `expandAl ``` +## Expandable rows + +To add expandable content to table cells, pass an array of `ExpandableContent` objects to the `expandedRows` prop of the `` component. Each expandable content object defines which cell can be expanded and what content to display when expanded. + +The `ExpandableContent` interface is defined as: + +```typescript +interface ExpandableContent { + /** The ID of the row containing the expandable cell (must match the id property in the row data) */ + row_id: number; + /** The column index (0-based) that should be expandable */ + column_id: number; + /** The content to display when the cell is expanded */ + content: ReactNode; +} +``` + +When a cell has expandable content: +- A compound expand toggle button appears in the cell +- Clicking the toggle expands the row to show the additional content below +- Only one expanded cell is shown per row at a time +- Clicking another expandable cell in the same row switches the expanded content + +### Expandable rows example + +```js file="./DataViewTableExpandableExample.tsx" + +``` + +## Sticky header and columns + +To enable sticky headers and columns, set the `isSticky` prop to `true` on the `` component. This keeps the table header and designated columns visible when scrolling. + +To make specific columns sticky, add the `isStickyColumn` property to the column's `props` in the column definition: + +```typescript +const columns: DataViewTh[] = [ + { cell: 'Column Name', props: { isStickyColumn: true } } +]; +``` + +When sticky headers and columns are enabled: +- The table header remains visible when scrolling vertically +- Columns marked with `isStickyColumn: true` remain visible when scrolling horizontally +- The table is wrapped in `OuterScrollContainer` and `InnerScrollContainer` components to enable sticky behavior +- Sticky columns can have additional styling like borders using `hasRightBorder` or `hasLeftBorder` props + +### Sticky header and columns example + +```js file="./DataViewTableStickyExample.tsx" + +``` + +## Draggable rows + +To enable drag-and-drop functionality for table rows, set the `isDraggable` prop to `true` on the `` component. This allows users to reorder rows by dragging them to a new position. + +When draggable rows are enabled: +- A drag handle icon appears at the beginning of each row +- Users can click and drag the handle or anywhere on the row to reorder rows +- Visual feedback is provided during the drag operation with a ghost row effect +- The new order is maintained after the drop + +The draggable functionality is built using the `useDraggableRows` hook internally, which manages the drag-and-drop state and DOM manipulation. + +### Draggable rows example + +```js file="./DataViewTableDraggableExample.tsx" + +``` + +### Interactive example +- Interactive example show how the different composable options work together. +- By toggling the toggles you can switch between them and observe the bahaviour + +```js file="./DataViewTableInteractiveExample.tsx" + +``` + ### Resizable columns 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: diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js index be42c242..5f6463e6 100644 --- a/packages/module/patternfly-docs/generated/index.js +++ b/packages/module/patternfly-docs/generated/index.js @@ -14,8 +14,8 @@ module.exports = { '/extensions/data-view/table/react': { id: "Table", title: "Data view table", - 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"}]], - examples: ["Table example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"], + toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"}],{"text":"Expandable rows"},[{"text":"Expandable rows example"}],{"text":"Sticky header and columns"},[{"text":"Sticky header and columns example"}],{"text":"Draggable rows"},[{"text":"Draggable rows example"},{"text":"Interactive 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"}]], + examples: ["Table example","Expandable rows example","Sticky header and columns example","Draggable rows example","Interactive example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"], section: "extensions", subsection: "Data view", source: "react", @@ -26,7 +26,7 @@ module.exports = { '/extensions/data-view/overview/extensions': { id: "Overview", title: "Data view overview", - toc: [[{"text":"Layout"},{"text":"Modularity"}],{"text":"Events context"},[{"text":"Row click subscription example"}]], + toc: [{"text":"How to structure and implement the data view"},[{"text":"Layout"},{"text":"Modularity"}],{"text":"Events context"},[{"text":"Row click subscription example"}]], examples: ["Layout","Modularity","Row click subscription example"], section: "extensions", subsection: "Data view", diff --git a/packages/module/src/DataViewTable/DataViewTable.tsx b/packages/module/src/DataViewTable/DataViewTable.tsx index fc96fe17..123c4df5 100644 --- a/packages/module/src/DataViewTable/DataViewTable.tsx +++ b/packages/module/src/DataViewTable/DataViewTable.tsx @@ -1,9 +1,11 @@ import { FC, ReactNode } from 'react'; import { TdProps, ThProps, TrProps, InnerScrollContainer } from '@patternfly/react-table'; import { DataViewTableTree, DataViewTableTreeProps } from '../DataViewTableTree'; -import { DataViewTableBasic, DataViewTableBasicProps } from '../DataViewTableBasic'; +import { DataViewTableBasic, DataViewTableBasicProps, ExpandableContent } from '../DataViewTableBasic'; import { DataViewThResizableProps } from '../DataViewTh/DataViewTh'; +export type { ExpandableContent }; + // Table head typings export type DataViewTh = | ReactNode diff --git a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx index 8208e9e6..4c272f19 100644 --- a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx +++ b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx @@ -1,15 +1,26 @@ -import { FC, useMemo } from 'react'; +import { FC, useMemo, useState, useRef } from 'react'; import { + ExpandableRowContent, + InnerScrollContainer, + OuterScrollContainer, Table, TableProps, Tbody, Td, Tr, } from '@patternfly/react-table'; +import { GripVerticalIcon } from '@patternfly/react-icons'; import { useInternalContext } from '../InternalContext'; import { DataViewTableHead } from '../DataViewTableHead'; import { DataViewTh, DataViewTr, isDataViewTdObject, isDataViewTrObject } from '../DataViewTable'; import { DataViewState } from '../DataView/DataView'; +import { useDraggableRows } from './hooks'; + +export interface ExpandableContent { + rowId: number; + columnId: number; + content: React.ReactNode; +} /** extends TableProps */ export interface DataViewTableBasicProps extends Omit { @@ -17,6 +28,8 @@ export interface DataViewTableBasicProps extends Omit> /** Table body states to be displayed when active */ @@ -25,15 +38,25 @@ export interface DataViewTableBasicProps extends Omit = ({ columns, rows, + expandedRows, ouiaId = 'DataViewTableBasic', headStates, bodyStates, hasResizableColumns, + isExpandable = false, + isSticky = false, + isDraggable = false, ...props }: DataViewTableBasicProps) => { const { selection, activeState, isSelectable } = useInternalContext(); @@ -42,45 +65,127 @@ export const DataViewTableBasic: FC = ({ const activeHeadState = useMemo(() => activeState ? headStates?.[activeState] : undefined, [ activeState, headStates ]); const activeBodyState = useMemo(() => activeState ? bodyStates?.[activeState] : undefined, [ activeState, bodyStates ]); + const [expandedRowsState, setExpandedRowsState] = useState>({}) + const [expandedColumnIndex, setExpandedColumnIndex] = useState>({}) + + const tableRef = useRef(null); + + const { + rowIds, + onDragStart, + onDragEnd, + onDrop, + onDropTbody, + onDragOver, + onDragLeave + } = useDraggableRows({ rows, tableRef }); + const renderedRows = useMemo(() => rows.map((row, rowIndex) => { const rowIsObject = isDataViewTrObject(row); + const isRowExpanded = expandedRowsState[rowIndex] || false; + const expandedColIndex = expandedColumnIndex[rowIndex]; + + // Get the first cell to extract the row ID + const rowData = rowIsObject ? row.row : row; + const firstCell = rowData[0]; + const rowId = isDataViewTdObject(firstCell) ? (firstCell as { id?: number }).id : undefined; + + // Find all expandable contents for this row + const rowExpandableContents = isExpandable ? expandedRows?.filter( + (content) => content.rowId === rowId + ) : []; + return ( - - {isSelectable && ( - { - onSelect?.(isSelecting, rowIsObject ? row : [ row ]); - }, - isSelected: isSelected?.(row) || false, - isDisabled: isSelectDisabled?.(row) || false, - }} - /> - )} - {(rowIsObject ? row.row : row).map((cell, colIndex) => { - const cellIsObject = isDataViewTdObject(cell); - return ( + + + {isSelectable && ( + { + onSelect?.(isSelecting, rowIsObject ? row : [ row ]); + }, + isSelected: isSelected?.(row) || false, + isDisabled: isSelectDisabled?.(row) || false, + }} + /> + )} + {isDraggable && ( - {cellIsObject ? cell.cell : cell} + - ); - })} - + )} + {(rowIsObject ? row.row : row).map((cell, colIndex) => { + const cellIsObject = isDataViewTdObject(cell); + const cellExpandableContent = isExpandable ? expandedRows?.find( + (content) => content.rowId === rowId && content.columnId === colIndex + ) : undefined; + return ( + { + console.log(`toggled compound expand for row ${rowIndex}, column ${colIndex}`); // eslint-disable-line no-console + setExpandedRowsState(prev => { + const isSameColumn = expandedColIndex === colIndex; + const wasExpanded = prev[rowIndex]; + return { ...prev, [rowIndex]: isSameColumn ? !wasExpanded : true }; + }); + setExpandedColumnIndex(prev => ({ ...prev, [rowIndex]: colIndex })); + }, + rowIndex, + columnIndex: colIndex + } + })} + data-ouia-component-id={`${ouiaId}-td-${rowIndex}-${colIndex}`} + > + {cellIsObject ? cell.cell : cell} + + ); + })} + + {rowExpandableContents?.map((expandableContent) => ( + + + + {expandableContent.content} + + + + ))} + ); - }), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId ]); + }), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId, expandedRowsState, expandedColumnIndex, expandedRows, rowIds, isDraggable, onDragOver, onDropTbody, onDragLeave, onDragEnd, onDragStart, isExpandable, onDrop ]); - return ( - - { activeHeadState || } - { activeBodyState || {renderedRows} } -
- ); + if (isSticky) { + return ( + + + + { activeHeadState || } + { activeBodyState || renderedRows } +
+
+
+ ); + } else { + return ( + + { activeHeadState || } + { activeBodyState || renderedRows } +
+ ); + } }; export default DataViewTableBasic; diff --git a/packages/module/src/DataViewTableBasic/hooks/index.ts b/packages/module/src/DataViewTableBasic/hooks/index.ts new file mode 100644 index 00000000..66781293 --- /dev/null +++ b/packages/module/src/DataViewTableBasic/hooks/index.ts @@ -0,0 +1 @@ +export * from './useDraggableRows'; diff --git a/packages/module/src/DataViewTableBasic/hooks/useDraggableRows.ts b/packages/module/src/DataViewTableBasic/hooks/useDraggableRows.ts new file mode 100644 index 00000000..73173270 --- /dev/null +++ b/packages/module/src/DataViewTableBasic/hooks/useDraggableRows.ts @@ -0,0 +1,198 @@ +import { useState, useRef, useMemo } from 'react'; +import { TbodyProps, TrProps } from '@patternfly/react-table'; +import styles from '@patternfly/react-styles/css/components/Table/table'; +import { DataViewTr } from '../../DataViewTable'; + +export interface UseDraggableRowsProps { + rows: DataViewTr[]; + tableRef: React.RefObject; + isDraggable?: boolean; +} + +export interface UseDraggableRowsReturn { + rowIds: string[]; + draggedItemId: string | null; + draggingToItemIndex: number | null; + isDragging: boolean; + itemOrder: string[]; + onDragStart: TrProps['onDragStart']; + onDragEnd: TrProps['onDragEnd']; + onDrop: TrProps['onDrop']; + onDropTbody: TbodyProps['onDrop']; + onDragOver: TbodyProps['onDragOver']; + onDragLeave: TbodyProps['onDragLeave']; +} + +export const useDraggableRows = ({ + rows, + tableRef +}: UseDraggableRowsProps): UseDraggableRowsReturn => { + const rowIds = useMemo(() => rows.map((_, index) => `row-${index}`), [rows]); + const [draggedItemId, setDraggedItemId] = useState(null); + const [draggingToItemIndex, setDraggingToItemIndex] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [itemOrder, setItemOrder] = useState(rowIds); + + const draggedItemIdRef = useRef(null); + + const moveItem = (arr: string[], itemId: string, toIndex: number): string[] => { + const fromIndex = arr.indexOf(itemId); + if (fromIndex === toIndex) { + return arr; + } + const temp = arr.splice(fromIndex, 1); + arr.splice(toIndex, 0, temp[0]); + + return arr; + }; + + const move = (order: string[]) => { + const tableNode = tableRef.current; + if (!tableNode) { + return; + } + + // Get all tbody elements (each tbody wraps one row) + const tbodyNodes = Array.from(tableNode.querySelectorAll('tbody')); + // Find the Tr inside each tbody to get its id + const currentOrder = tbodyNodes + .map(tbody => tbody.querySelector('tr[id]')) + .filter((tr): tr is HTMLTableRowElement => tr !== null) + .map(tr => tr.id); + + if (currentOrder.every((id, i) => id === order[i])) { + return; + } + + // Reorder tbody elements based on the new order + order.forEach((id) => { + const tbody = tbodyNodes.find(tbody => { + const tr = tbody.querySelector('tr[id]'); + return tr?.id === id; + }); + if (tbody && tbody.parentNode) { + tbody.parentNode.appendChild(tbody); + } + }); + }; + + const isValidDrop = (evt: React.DragEvent): boolean => { + if (!tableRef.current) { + return false; + } + const tableRect = tableRef.current.getBoundingClientRect(); + return ( + evt.clientX > tableRect.x && + evt.clientX < tableRect.x + tableRect.width && + evt.clientY > tableRect.y && + evt.clientY < tableRect.y + tableRect.height + ); + }; + + const onDragCancel = () => { + if (tableRef.current) { + const allRows = tableRef.current.querySelectorAll('tr[id]'); + allRows.forEach((el) => { + el.classList.remove(styles.modifiers.ghostRow); + el.setAttribute('aria-pressed', 'false'); + }); + } + draggedItemIdRef.current = null; + setDraggedItemId(null); + setDraggingToItemIndex(null); + setIsDragging(false); + }; + + const onDragStart: TrProps['onDragStart'] = (evt) => { + evt.dataTransfer.effectAllowed = 'move'; + evt.dataTransfer.setData('text/plain', evt.currentTarget.id); + const itemId = evt.currentTarget.id; + + evt.currentTarget.classList.add(styles.modifiers.ghostRow); + evt.currentTarget.setAttribute('aria-pressed', 'true'); + + draggedItemIdRef.current = itemId; + setDraggedItemId(itemId); + setIsDragging(true); + }; + + const onDragLeave: TbodyProps['onDragLeave'] = (evt) => { + if (!isValidDrop(evt)) { + move(itemOrder); + setDraggingToItemIndex(null); + } + }; + + const onDragOver: TbodyProps['onDragOver'] = (evt) => { + evt.preventDefault(); + + const currentDraggedId = draggedItemIdRef.current; + if (!tableRef.current || !currentDraggedId) { + return; + } + + const curListItem = (evt.target as HTMLTableSectionElement).closest('tr[id]'); + if (!curListItem || !tableRef.current.contains(curListItem) || curListItem.id === currentDraggedId) { + return; + } + + const dragId = curListItem.id; + // Get the current DOM order by finding all tbody > tr[id] pairs + const tbodyNodes = Array.from(tableRef.current.querySelectorAll('tbody')); + const currentDomOrder = tbodyNodes + .map(tbody => tbody.querySelector('tr[id]')) + .filter((tr): tr is HTMLTableRowElement => tr !== null) + .map(tr => tr.id); + + const newDraggingToItemIndex = currentDomOrder.indexOf(dragId); + + if (newDraggingToItemIndex !== draggingToItemIndex) { + // Use currentDomOrder as the base since DOM has already been reordered during drag + const newOrder = moveItem([...currentDomOrder], currentDraggedId, newDraggingToItemIndex); + move(newOrder); + setDraggingToItemIndex(newDraggingToItemIndex); + } + }; + + const onDrop: TrProps['onDrop'] = (evt) => { + if (isValidDrop(evt) && tableRef.current) { + // Read the current DOM order and commit it - this is the simplest approach + const tbodyNodes = Array.from(tableRef.current.querySelectorAll('tbody')); + const finalOrder = tbodyNodes + .map(tbody => tbody.querySelector('tr[id]')) + .filter((tr): tr is HTMLTableRowElement => tr !== null) + .map(tr => tr.id); + setItemOrder(finalOrder); + } else { + onDragCancel(); + } + }; + + const onDropTbody: TbodyProps['onDrop'] = (evt) => { + onDrop(evt as unknown as React.DragEvent); + }; + + const onDragEnd: TrProps['onDragEnd'] = (evt) => { + const target = evt.target as HTMLTableRowElement; + target.classList.remove(styles.modifiers.ghostRow); + target.setAttribute('aria-pressed', 'false'); + draggedItemIdRef.current = null; + setDraggedItemId(null); + setDraggingToItemIndex(null); + setIsDragging(false); + }; + + return { + rowIds, + draggedItemId, + draggingToItemIndex, + isDragging, + itemOrder, + onDragStart, + onDragEnd, + onDrop, + onDropTbody, + onDragOver, + onDragLeave + }; +}; diff --git a/packages/module/src/DataViewTableHead/DataViewTableHead.tsx b/packages/module/src/DataViewTableHead/DataViewTableHead.tsx index 81fc1e19..d410b021 100644 --- a/packages/module/src/DataViewTableHead/DataViewTableHead.tsx +++ b/packages/module/src/DataViewTableHead/DataViewTableHead.tsx @@ -14,6 +14,8 @@ export interface DataViewTableHeadProps extends TheadProps { ouiaId?: string; /** @hide Indicates whether table is resizable */ hasResizableColumns?: boolean; + /** Indicates whether table rows are draggable */ + isDraggable?: boolean; } export const DataViewTableHead: FC = ({ @@ -21,6 +23,7 @@ export const DataViewTableHead: FC = ({ columns, ouiaId = 'DataViewTableHead', hasResizableColumns, + isDraggable = false, ...props }: DataViewTableHeadProps) => { const { selection } = useInternalContext(); @@ -31,6 +34,9 @@ export const DataViewTableHead: FC = ({ onSelect && isSelected && !isTreeTable ? ( ) : null, + isDraggable ? ( + + ) : null, ...columns.map((column, index) => ( = ({ /> )) ], - [ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns ] + [ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns, isDraggable ] ); return (