From 452d95454a05780db295a865b505f9b3cfc7e0be Mon Sep 17 00:00:00 2001 From: LightOfHeaven1994 Date: Fri, 7 Aug 2026 17:23:43 +0200 Subject: [PATCH 1/5] feat(DataViewTable): introduce data view table --- package-lock.json | 30 +++++ package.json | 1 + src/components/DataViewTable/DataViewTable.js | 96 +++++++++++++ .../DataViewTable/DataViewTable.stories.js | 126 ++++++++++++++++++ .../DataViewTable/helpers/bodyStates.js | 36 +++++ .../helpers/getDataViewStateProps.js | 58 ++++++++ src/components/DataViewTable/helpers/index.js | 2 + .../DataViewTable/helpers/toDataViewProps.js | 27 ++++ .../hooks/useTableToolsForDataView.js | 76 +++++++++++ src/components/DataViewTable/index.js | 1 + src/components/index.js | 1 + 11 files changed, 454 insertions(+) create mode 100644 src/components/DataViewTable/DataViewTable.js create mode 100644 src/components/DataViewTable/DataViewTable.stories.js create mode 100644 src/components/DataViewTable/helpers/bodyStates.js create mode 100644 src/components/DataViewTable/helpers/getDataViewStateProps.js create mode 100644 src/components/DataViewTable/helpers/index.js create mode 100644 src/components/DataViewTable/helpers/toDataViewProps.js create mode 100644 src/components/DataViewTable/hooks/useTableToolsForDataView.js create mode 100644 src/components/DataViewTable/index.js diff --git a/package-lock.json b/package-lock.json index c71c25c..7a35f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "@patternfly/patternfly": "^6.0.0", "@patternfly/react-component-groups": "^6.0.0", "@patternfly/react-core": "^6.0.0", + "@patternfly/react-data-view": "^6.5.0", "@patternfly/react-table": "^6.0.0", "@redhat-cloud-services/frontend-components": ">= 6.1.0", "@redhat-cloud-services/frontend-components-utilities": ">= 6.1.0", @@ -3181,6 +3182,25 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-data-view": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.5.0.tgz", + "integrity": "sha512-QTj8eg/pwchdxSpapqU3a6MZG7syiwzXy1La2hgAJtvQTR9ltkIg/GJJ4ota2O5wHzdSAVzTTdxiNuP33wmvbQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@patternfly/react-component-groups": "^6.1.0", + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-table": "^6.4.0", + "clsx": "^2.1.1", + "react-jss": "^10.10.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-drag-drop": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.4.1.tgz", @@ -7311,6 +7331,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index 861bae1..7a0e695 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@patternfly/patternfly": "^6.0.0", "@patternfly/react-component-groups": "^6.0.0", "@patternfly/react-core": "^6.0.0", + "@patternfly/react-data-view": "^6.5.0", "@patternfly/react-table": "^6.0.0", "@redhat-cloud-services/frontend-components": ">= 6.1.0", "@redhat-cloud-services/frontend-components-utilities": ">= 6.1.0", diff --git a/src/components/DataViewTable/DataViewTable.js b/src/components/DataViewTable/DataViewTable.js new file mode 100644 index 0000000..d02b85c --- /dev/null +++ b/src/components/DataViewTable/DataViewTable.js @@ -0,0 +1,96 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { + DataView, + DataViewTable as PatternFlyDataViewTable, + DataViewToolbar, +} from '@patternfly/react-data-view'; +import { Pagination, PaginationVariant } from '@patternfly/react-core'; + +import { TableStateProvider } from '~/components'; + +import useTableToolsForDataView from './hooks/useTableToolsForDataView'; + +/** + * DataView-based variant of TableToolsTable. + * Reuses useTableTools (via useTableToolsForDataView) and renders PatternFly Data View. + * + * @param {object} props Component props + * @param {Array|Function} props.items Items array or async fetch function + * @param {Array} props.columns TableTools column definitions + * @param {boolean} [props.loading] External loading flag + * @param {object} [props.error] External error + * @param {number} [props.total] External total count + * @param {object} [props.options] Options passed to useTableTools + * @returns {React.ReactElement} DataView table + * + * @group Components + */ +const DataViewTable = ({ + loading, + items, + error, + total, + columns, + toolbarProps, + options, + ...rest +}) => { + const { + columns: dataViewColumns, + rows: dataViewRows, + activeState, + headStates, + bodyStates, + pagination, + } = useTableToolsForDataView({ + loading, + items, + error, + total, + columns, + toolbarProps, + options, + }); + + return ( + + } + /> + + + ) + } + /> + + ); +}; + +DataViewTable.propTypes = { + items: propTypes.oneOfType([propTypes.array, propTypes.func]).isRequired, + columns: propTypes.array.isRequired, + loading: propTypes.bool, + error: propTypes.object, + total: propTypes.number, + toolbarProps: propTypes.object, + options: propTypes.object, +}; + +const DataViewTableWithProvider = (props) => ( + + + +); + +export default DataViewTableWithProvider; diff --git a/src/components/DataViewTable/DataViewTable.stories.js b/src/components/DataViewTable/DataViewTable.stories.js new file mode 100644 index 0000000..042708d --- /dev/null +++ b/src/components/DataViewTable/DataViewTable.stories.js @@ -0,0 +1,126 @@ +import React from 'react'; +import defaultStoryMeta from '~/support/defaultStoryMeta'; +import columns from '~/support/factories/columns'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { TableStateProvider } from '~/components'; +import useExampleDataQuery from '~/support/hooks/useExampleDataQuery'; +import paginationSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/pagination'; + +import DataViewTable from './DataViewTable'; + +const meta = { + title: 'DataViewTable', + component: DataViewTable, + ...defaultStoryMeta, +}; + +export default meta; + +const queryClient = new QueryClient(); + +const defaultOptions = { + serialisers: { + pagination: paginationSerialiser, + }, +}; + +const CommonExample = () => { + const { + loading, + result: { data, meta: { total } = {} } = {}, + error, + } = useExampleDataQuery({ + endpoint: '/api', + useTableState: true, + }); + + return ( + + ); +}; + +export const Common = { + decorators: [ + (Story) => ( + + + + + + ), + ], + render: (args) => , +}; + +const WithErrorPassedExample = () => { + const { + loading, + result: { data, meta: { total } = {} } = {}, + error, + } = useExampleDataQuery({ endpoint: '/api/error' }); + return ( + + ); +}; + +export const WithErrorPassed = { + decorators: [ + (Story) => ( + + + + + + ), + ], + render: (args) => , +}; + +const EmptyExample = () => { + const { + loading, + result: { data, meta: { total } = {} } = {}, + error, + } = useExampleDataQuery({ + endpoint: '/api', + params: { total: 0 }, + }); + + return ( + + ); +}; + +export const Empty = { + decorators: [ + (Story) => ( + + + + + + ), + ], + render: (args) => , +}; diff --git a/src/components/DataViewTable/helpers/bodyStates.js b/src/components/DataViewTable/helpers/bodyStates.js new file mode 100644 index 0000000..f275dcb --- /dev/null +++ b/src/components/DataViewTable/helpers/bodyStates.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { ErrorState } from '@patternfly/react-component-groups'; +import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Tbody, Td, Tr } from '@patternfly/react-table'; + +export const getErrorBodyState = (columnsCount) => ( + + + + + + + +); + +export const getDefaultEmptyBodyState = (columns) => ( + + + + + + There are no matching data to be displayed. + + + + + +); diff --git a/src/components/DataViewTable/helpers/getDataViewStateProps.js b/src/components/DataViewTable/helpers/getDataViewStateProps.js new file mode 100644 index 0000000..976a255 --- /dev/null +++ b/src/components/DataViewTable/helpers/getDataViewStateProps.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { DataViewState } from '@patternfly/react-data-view'; +import { + SkeletonTableBody, + SkeletonTableHead, +} from '@patternfly/react-component-groups'; + +import { getDefaultEmptyBodyState, getErrorBodyState } from './bodyStates'; + +const DEFAULT_SKELETON_ROWS = 10; + +/** + * Builds DataView activeState + head/body state content. + * + * @param {object} params + * @param {boolean} params.loading + * @param {object} [params.error] + * @param {Array} params.rows DataView rows (from toDataViewProps) + * @param {Array} params.columns DataView columns (from toDataViewProps) + * @param {React.ReactNode} [params.emptyState] Optional custom empty body + * @param {number} [params.perPage] Skeleton row count hint + * @returns {{ activeState: string|undefined, headStates: object, bodyStates: object }} + */ +export const getDataViewStateProps = ({ + loading, + error, + rows, + columns, + emptyState, + perPage, +}) => { + let activeState; + + if (loading) { + activeState = DataViewState.loading; + } else if (error) { + activeState = DataViewState.error; + } else if (!rows?.length) { + activeState = DataViewState.empty; + } + + return { + activeState, + headStates: { + loading: , + }, + bodyStates: { + loading: ( + + ), + error: getErrorBodyState(columns.length), + empty: emptyState || getDefaultEmptyBodyState(columns), + }, + }; +}; diff --git a/src/components/DataViewTable/helpers/index.js b/src/components/DataViewTable/helpers/index.js new file mode 100644 index 0000000..9e24de8 --- /dev/null +++ b/src/components/DataViewTable/helpers/index.js @@ -0,0 +1,2 @@ +export { toDataViewProps } from './toDataViewProps'; +export { getDataViewStateProps } from './getDataViewStateProps'; diff --git a/src/components/DataViewTable/helpers/toDataViewProps.js b/src/components/DataViewTable/helpers/toDataViewProps.js new file mode 100644 index 0000000..4bfc766 --- /dev/null +++ b/src/components/DataViewTable/helpers/toDataViewProps.js @@ -0,0 +1,27 @@ +/** + * Adapts deprecated PatternFly Table props from useTableTools into + * PatternFly DataViewTable columns/rows. + * + * Filter out rows without an `item` to avoid rendering empty/error placeholder rows. + * + * @param {object} tableProps `tableProps` returned by useTableTools + * @returns {{ columns: Array, rows: Array }} DataView-compatible props + */ +export const toDataViewProps = (tableProps = {}) => { + const columns = (tableProps.cells || []).map((column) => + typeof column.title === 'string' || column.title == null + ? column.title + : { cell: column.title }, + ); + + const rows = (tableProps.rows || []) + .filter((row) => row.item != null) + .map((row, index) => ({ + id: String(row.item.itemId ?? row.item.id ?? index), + row: (row.cells || []).map((cell) => + typeof cell?.title === 'function' ? cell.title() : cell?.title, + ), + })); + + return { columns, rows }; +}; diff --git a/src/components/DataViewTable/hooks/useTableToolsForDataView.js b/src/components/DataViewTable/hooks/useTableToolsForDataView.js new file mode 100644 index 0000000..c37c395 --- /dev/null +++ b/src/components/DataViewTable/hooks/useTableToolsForDataView.js @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; + +import useTableTools from '~/hooks/useTableTools'; + +import { toDataViewProps, getDataViewStateProps } from '../helpers'; + +/** + * Adapter around useTableTools that reshapes its return value for Data View. + * + * @param {object} props + * @param {boolean} [props.loading] + * @param {Array|Function} props.items + * @param {object} [props.error] + * @param {number} [props.total] + * @param {Array} props.columns + * @param {object} [props.toolbarProps] + * @param {object} [props.options] + * @returns {object} Props ready for DataView / DataViewTable / DataViewToolbar + * + * @group Hooks + */ +const useTableToolsForDataView = ({ + loading: externalLoading, + items: externalItems, + error: externalError, + total: externalTotal, + columns, + toolbarProps: toolbarPropsProp, + options = {}, +}) => { + const { loading, tableProps, toolbarProps } = useTableTools( + externalLoading, + externalItems, + externalError, + externalTotal, + { + columns, + toolbarProps: toolbarPropsProp, + ...options, + }, + ); + + const { columns: dataViewColumns, rows: dataViewRows } = useMemo( + () => toDataViewProps(tableProps), + [tableProps], + ); + + const { activeState, headStates, bodyStates } = useMemo( + () => + getDataViewStateProps({ + loading, + error: externalError, + rows: dataViewRows, + columns: dataViewColumns, + perPage: toolbarProps.pagination?.perPage, + }), + [ + loading, + externalError, + dataViewRows, + dataViewColumns, + toolbarProps.pagination?.perPage, + ], + ); + + return { + columns: dataViewColumns, + rows: dataViewRows, + activeState, + headStates, + bodyStates, + pagination: toolbarProps.pagination, + }; +}; + +export default useTableToolsForDataView; diff --git a/src/components/DataViewTable/index.js b/src/components/DataViewTable/index.js new file mode 100644 index 0000000..747e39e --- /dev/null +++ b/src/components/DataViewTable/index.js @@ -0,0 +1 @@ +export { default } from './DataViewTable'; diff --git a/src/components/index.js b/src/components/index.js index e35b5c0..9078b72 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -4,3 +4,4 @@ export { default as StaticTableToolsTable } from './StaticTableToolsTable'; export { default as NoResultsTable } from './NoResultsTable'; export { default as FilterModal } from './FilterModal'; export { default as TableViewToggle } from './TableViewToggle'; +export { default as DataViewTable } from './DataViewTable'; From bf1a84c7eb3ecbc54e1c5e669eda2dd758626cfd Mon Sep 17 00:00:00 2001 From: LightOfHeaven1994 Date: Fri, 7 Aug 2026 18:03:23 +0200 Subject: [PATCH 2/5] feat(DataViewTable): add sorting --- .../DataViewTable/DataViewTable.stories.js | 2 ++ .../DataViewTable/helpers/toDataViewProps.js | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/DataViewTable/DataViewTable.stories.js b/src/components/DataViewTable/DataViewTable.stories.js index 042708d..3ec7b47 100644 --- a/src/components/DataViewTable/DataViewTable.stories.js +++ b/src/components/DataViewTable/DataViewTable.stories.js @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TableStateProvider } from '~/components'; import useExampleDataQuery from '~/support/hooks/useExampleDataQuery'; import paginationSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/pagination'; +import sortSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/sort'; import DataViewTable from './DataViewTable'; @@ -21,6 +22,7 @@ const queryClient = new QueryClient(); const defaultOptions = { serialisers: { pagination: paginationSerialiser, + sort: sortSerialiser, }, }; diff --git a/src/components/DataViewTable/helpers/toDataViewProps.js b/src/components/DataViewTable/helpers/toDataViewProps.js index 4bfc766..605276e 100644 --- a/src/components/DataViewTable/helpers/toDataViewProps.js +++ b/src/components/DataViewTable/helpers/toDataViewProps.js @@ -8,19 +8,31 @@ * @returns {{ columns: Array, rows: Array }} DataView-compatible props */ export const toDataViewProps = (tableProps = {}) => { - const columns = (tableProps.cells || []).map((column) => - typeof column.title === 'string' || column.title == null - ? column.title - : { cell: column.title }, - ); + const addSortIfSortable = (column, index) => { + if (column.sortable) { + return { + sort: { + sortBy: tableProps.sortBy, + onSort: tableProps.onSort, + columnIndex: index, + }, + }; + } + return {}; + }; + + const columns = (tableProps.cells || []).map((column, index) => { + return { + cell: column.title, + props: { ...addSortIfSortable(column, index) }, + }; + }); const rows = (tableProps.rows || []) .filter((row) => row.item != null) .map((row, index) => ({ id: String(row.item.itemId ?? row.item.id ?? index), - row: (row.cells || []).map((cell) => - typeof cell?.title === 'function' ? cell.title() : cell?.title, - ), + row: (row.cells || []).map((cell) => cell?.title), })); return { columns, rows }; From 1892b6a6966c532941044f95750a834695171267 Mon Sep 17 00:00:00 2001 From: LightOfHeaven1994 Date: Mon, 10 Aug 2026 07:47:12 +0200 Subject: [PATCH 3/5] feat(DataViewTable): add actions --- jest.config.js | 4 +++- src/components/DataViewTable/DataViewTable.js | 3 ++- .../DataViewTable/DataViewTable.stories.js | 2 ++ src/components/DataViewTable/helpers/index.js | 1 + .../DataViewTable/helpers/toDataViewActions.js | 18 ++++++++++++++++++ .../hooks/useTableToolsForDataView.js | 15 +++++++++++++-- 6 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/components/DataViewTable/helpers/toDataViewActions.js diff --git a/jest.config.js b/jest.config.js index 75650a6..b8f8e19 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,7 +15,9 @@ module.exports = { '!src/support/**/*.{js,jsx}', ], roots: ['/src/'], - transformIgnorePatterns: ['/node_modules/(?!(@faker-js)/)'], + transformIgnorePatterns: [ + '/node_modules/(?!(@faker-js|@patternfly/react-tokens|@patternfly/react-data-view)/)', + ], moduleNameMapper: { '\\.(css|scss|svg)$': 'identity-obj-proxy', diff --git a/src/components/DataViewTable/DataViewTable.js b/src/components/DataViewTable/DataViewTable.js index d02b85c..c384a7f 100644 --- a/src/components/DataViewTable/DataViewTable.js +++ b/src/components/DataViewTable/DataViewTable.js @@ -42,7 +42,7 @@ const DataViewTable = ({ activeState, headStates, bodyStates, - pagination, + toolbarProps: { pagination, actions } = {}, } = useTableToolsForDataView({ loading, items, @@ -57,6 +57,7 @@ const DataViewTable = ({ } + actions={actions} /> { loading={loading} options={{ ...defaultOptions, + actions, }} /> ); diff --git a/src/components/DataViewTable/helpers/index.js b/src/components/DataViewTable/helpers/index.js index 9e24de8..907b6de 100644 --- a/src/components/DataViewTable/helpers/index.js +++ b/src/components/DataViewTable/helpers/index.js @@ -1,2 +1,3 @@ export { toDataViewProps } from './toDataViewProps'; export { getDataViewStateProps } from './getDataViewStateProps'; +export { toDataViewActions } from './toDataViewActions'; diff --git a/src/components/DataViewTable/helpers/toDataViewActions.js b/src/components/DataViewTable/helpers/toDataViewActions.js new file mode 100644 index 0000000..8c59926 --- /dev/null +++ b/src/components/DataViewTable/helpers/toDataViewActions.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; + +export const toDataViewActions = (actions) => + (actions || []).filter(Boolean).map((action, i) => { + console.log('DEBUG: action', action); + if (React.isValidElement(action)) { + return {action}; + } + const { label, onClick } = action; + return typeof label === 'string' ? ( + + ) : ( + {label} + ); + }); diff --git a/src/components/DataViewTable/hooks/useTableToolsForDataView.js b/src/components/DataViewTable/hooks/useTableToolsForDataView.js index c37c395..6d008e0 100644 --- a/src/components/DataViewTable/hooks/useTableToolsForDataView.js +++ b/src/components/DataViewTable/hooks/useTableToolsForDataView.js @@ -2,7 +2,11 @@ import { useMemo } from 'react'; import useTableTools from '~/hooks/useTableTools'; -import { toDataViewProps, getDataViewStateProps } from '../helpers'; +import { + toDataViewProps, + getDataViewStateProps, + toDataViewActions, +} from '../helpers'; /** * Adapter around useTableTools that reshapes its return value for Data View. @@ -39,6 +43,10 @@ const useTableToolsForDataView = ({ ...options, }, ); + const actions = useMemo( + () => toDataViewActions(toolbarProps.actionsConfig?.actions), + [toolbarProps.actionsConfig?.actions], + ); const { columns: dataViewColumns, rows: dataViewRows } = useMemo( () => toDataViewProps(tableProps), @@ -69,7 +77,10 @@ const useTableToolsForDataView = ({ activeState, headStates, bodyStates, - pagination: toolbarProps.pagination, + toolbarProps: { + pagination: toolbarProps.pagination, + actions, + }, }; }; From dabe1923e0f33e16d84f9c56e77341e3e509371b Mon Sep 17 00:00:00 2001 From: LightOfHeaven1994 Date: Mon, 10 Aug 2026 08:16:43 +0200 Subject: [PATCH 4/5] feat(DataViewTable): add export --- src/components/DataViewTable/DataViewTable.js | 1 + .../DataViewTable/DataViewTable.stories.js | 2 ++ src/components/DataViewTable/helpers/index.js | 1 + .../helpers/toDataViewActions.js | 1 - .../DataViewTable/helpers/toDataViewExport.js | 16 +++++++++++++ .../hooks/useTableToolsForDataView.js | 23 +++++++++++++++---- 6 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/components/DataViewTable/helpers/toDataViewExport.js diff --git a/src/components/DataViewTable/DataViewTable.js b/src/components/DataViewTable/DataViewTable.js index c384a7f..3255746 100644 --- a/src/components/DataViewTable/DataViewTable.js +++ b/src/components/DataViewTable/DataViewTable.js @@ -58,6 +58,7 @@ const DataViewTable = ({ } actions={actions} + ouiaId="data-view-table-toolbar" /> { loading, result: { data, meta: { total } = {} } = {}, error, + exporter, } = useExampleDataQuery({ endpoint: '/api', useTableState: true, @@ -47,6 +48,7 @@ const CommonExample = () => { options={{ ...defaultOptions, actions, + exporter, }} /> ); diff --git a/src/components/DataViewTable/helpers/index.js b/src/components/DataViewTable/helpers/index.js index 907b6de..be6fba6 100644 --- a/src/components/DataViewTable/helpers/index.js +++ b/src/components/DataViewTable/helpers/index.js @@ -1,3 +1,4 @@ export { toDataViewProps } from './toDataViewProps'; export { getDataViewStateProps } from './getDataViewStateProps'; export { toDataViewActions } from './toDataViewActions'; +export { toDataViewExport } from './toDataViewExport'; diff --git a/src/components/DataViewTable/helpers/toDataViewActions.js b/src/components/DataViewTable/helpers/toDataViewActions.js index 8c59926..184fa21 100644 --- a/src/components/DataViewTable/helpers/toDataViewActions.js +++ b/src/components/DataViewTable/helpers/toDataViewActions.js @@ -3,7 +3,6 @@ import { Button } from '@patternfly/react-core'; export const toDataViewActions = (actions) => (actions || []).filter(Boolean).map((action, i) => { - console.log('DEBUG: action', action); if (React.isValidElement(action)) { return {action}; } diff --git a/src/components/DataViewTable/helpers/toDataViewExport.js b/src/components/DataViewTable/helpers/toDataViewExport.js new file mode 100644 index 0000000..bd49946 --- /dev/null +++ b/src/components/DataViewTable/helpers/toDataViewExport.js @@ -0,0 +1,16 @@ +import React from 'react'; +import DownloadButton from '@redhat-cloud-services/frontend-components/DownloadButton'; + +/** + * Adapts PrimaryToolbar `exportConfig` into a DataViewToolbar actions slot node. + * + * @param {object} [exportConfig] exportConfig from useExport / toolbarProps + * @returns {React.ReactElement|null} DownloadButton or null when export is off + */ +export const toDataViewExport = (exportConfig) => { + if (!exportConfig?.onSelect && !exportConfig?.extraItems) { + return null; + } + + return ; +}; diff --git a/src/components/DataViewTable/hooks/useTableToolsForDataView.js b/src/components/DataViewTable/hooks/useTableToolsForDataView.js index 6d008e0..0353f11 100644 --- a/src/components/DataViewTable/hooks/useTableToolsForDataView.js +++ b/src/components/DataViewTable/hooks/useTableToolsForDataView.js @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import useTableTools from '~/hooks/useTableTools'; @@ -6,6 +6,7 @@ import { toDataViewProps, getDataViewStateProps, toDataViewActions, + toDataViewExport, } from '../helpers'; /** @@ -43,10 +44,22 @@ const useTableToolsForDataView = ({ ...options, }, ); - const actions = useMemo( - () => toDataViewActions(toolbarProps.actionsConfig?.actions), - [toolbarProps.actionsConfig?.actions], - ); + + const actions = useMemo(() => { + const actionNodes = toDataViewActions(toolbarProps.actionsConfig?.actions); + const exportNode = toDataViewExport(toolbarProps.exportConfig); + + if (!actionNodes.length && !exportNode) { + return undefined; + } + + return ( + <> + {actionNodes} + {exportNode} + + ); + }, [toolbarProps.actionsConfig?.actions, toolbarProps.exportConfig]); const { columns: dataViewColumns, rows: dataViewRows } = useMemo( () => toDataViewProps(tableProps), From 221f6120e5626162105ad51c4c79a3db7a2f4e38 Mon Sep 17 00:00:00 2001 From: LightOfHeaven1994 Date: Mon, 10 Aug 2026 16:05:01 +0200 Subject: [PATCH 5/5] feat(DataViewTable): add table variant --- src/components/DataViewTable/DataViewTable.js | 56 ++-------- .../DataViewTable/DataViewTable.stories.js | 15 +-- .../helpers/getDataViewStateProps.js | 12 -- .../hooks/useTableToolsForDataView.js | 77 ++++++------- .../DeprecatedTable/DeprecatedTable.js | 103 ++++++++++++++++++ .../DeprecatedTable.stories.js} | 0 .../DeprecatedTable.test.js} | 6 +- .../DeprecatedTableExperiments.stories.js} | 0 .../DeprecatedTable/PrimaryToolbar.js | 20 ++++ .../hooks/useTableToolsForDeprecatedTable.js | 56 ++++++++++ src/components/DeprecatedTable/index.js | 1 + .../{TableToolsTable => }/TableToolsTable.js | 71 ++++-------- src/components/TableToolsTable/index.js | 1 - src/components/constants.js | 7 ++ src/components/index.js | 1 + 15 files changed, 265 insertions(+), 161 deletions(-) create mode 100644 src/components/DeprecatedTable/DeprecatedTable.js rename src/components/{TableToolsTable/TableToolsTable.stories.js => DeprecatedTable/DeprecatedTable.stories.js} (100%) rename src/components/{TableToolsTable/TableToolsTable.test.js => DeprecatedTable/DeprecatedTable.test.js} (85%) rename src/components/{TableToolsTable/TableToolsTableExperiments.stories.js => DeprecatedTable/DeprecatedTableExperiments.stories.js} (100%) create mode 100644 src/components/DeprecatedTable/PrimaryToolbar.js create mode 100644 src/components/DeprecatedTable/hooks/useTableToolsForDeprecatedTable.js create mode 100644 src/components/DeprecatedTable/index.js rename src/components/{TableToolsTable => }/TableToolsTable.js (67%) delete mode 100644 src/components/TableToolsTable/index.js create mode 100644 src/components/constants.js diff --git a/src/components/DataViewTable/DataViewTable.js b/src/components/DataViewTable/DataViewTable.js index 3255746..3026b9e 100644 --- a/src/components/DataViewTable/DataViewTable.js +++ b/src/components/DataViewTable/DataViewTable.js @@ -7,35 +7,21 @@ import { } from '@patternfly/react-data-view'; import { Pagination, PaginationVariant } from '@patternfly/react-core'; -import { TableStateProvider } from '~/components'; - import useTableToolsForDataView from './hooks/useTableToolsForDataView'; /** - * DataView-based variant of TableToolsTable. - * Reuses useTableTools (via useTableToolsForDataView) and renders PatternFly Data View. + * Data View presentation variant. * - * @param {object} props Component props - * @param {Array|Function} props.items Items array or async fetch function - * @param {Array} props.columns TableTools column definitions - * @param {boolean} [props.loading] External loading flag - * @param {object} [props.error] External error - * @param {number} [props.total] External total count - * @param {object} [props.options] Options passed to useTableTools - * @returns {React.ReactElement} DataView table + * @param {object} props Component props (useTableTools output) + * @param {boolean} [props.loading] Loading state + * @param {object} [props.error] Error state + * @param {object} [props.tableProps] PatternFly table props from useTableTools + * @param {object} [props.toolbarProps] Toolbar props from useTableTools * * @group Components + * @returns {React.ReactElement} DataView table */ -const DataViewTable = ({ - loading, - items, - error, - total, - columns, - toolbarProps, - options, - ...rest -}) => { +const DataViewTable = (props) => { const { columns: dataViewColumns, rows: dataViewRows, @@ -43,15 +29,7 @@ const DataViewTable = ({ headStates, bodyStates, toolbarProps: { pagination, actions } = {}, - } = useTableToolsForDataView({ - loading, - items, - error, - total, - columns, - toolbarProps, - options, - }); + } = useTableToolsForDataView(props); return ( @@ -61,12 +39,11 @@ const DataViewTable = ({ ouiaId="data-view-table-toolbar" /> ( - - - -); - -export default DataViewTableWithProvider; +export default DataViewTable; diff --git a/src/components/DataViewTable/DataViewTable.stories.js b/src/components/DataViewTable/DataViewTable.stories.js index b258866..fe30339 100644 --- a/src/components/DataViewTable/DataViewTable.stories.js +++ b/src/components/DataViewTable/DataViewTable.stories.js @@ -2,17 +2,15 @@ import React from 'react'; import defaultStoryMeta from '~/support/defaultStoryMeta'; import columns from '~/support/factories/columns'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { TableStateProvider } from '~/components'; +import { TableStateProvider, TableToolsTable } from '~/components'; import useExampleDataQuery from '~/support/hooks/useExampleDataQuery'; import paginationSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/pagination'; import sortSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/sort'; import { actions } from '~/support/constants'; -import DataViewTable from './DataViewTable'; - const meta = { title: 'DataViewTable', - component: DataViewTable, + component: TableToolsTable, ...defaultStoryMeta, }; @@ -39,7 +37,8 @@ const CommonExample = () => { }); return ( - { error, } = useExampleDataQuery({ endpoint: '/api/error' }); return ( - { }); return ( - { - const { loading, tableProps, toolbarProps } = useTableTools( - externalLoading, - externalItems, - externalError, - externalTotal, - { - columns, - toolbarProps: toolbarPropsProp, - ...options, - }, - ); - const actions = useMemo(() => { const actionNodes = toDataViewActions(toolbarProps.actionsConfig?.actions); const exportNode = toDataViewExport(toolbarProps.exportConfig); @@ -70,31 +50,42 @@ const useTableToolsForDataView = ({ () => getDataViewStateProps({ loading, - error: externalError, + error, rows: dataViewRows, columns: dataViewColumns, perPage: toolbarProps.pagination?.perPage, }), [ loading, - externalError, + error, dataViewRows, dataViewColumns, toolbarProps.pagination?.perPage, ], ); - return { - columns: dataViewColumns, - rows: dataViewRows, - activeState, - headStates, - bodyStates, - toolbarProps: { - pagination: toolbarProps.pagination, + return useMemo( + () => ({ + columns: dataViewColumns, + rows: dataViewRows, + activeState, + headStates, + bodyStates, + toolbarProps: { + pagination: toolbarProps.pagination, + actions, + }, + }), + [ + dataViewColumns, + dataViewRows, + activeState, + headStates, + bodyStates, + toolbarProps.pagination, actions, - }, - }; + ], + ); }; export default useTableToolsForDataView; diff --git a/src/components/DeprecatedTable/DeprecatedTable.js b/src/components/DeprecatedTable/DeprecatedTable.js new file mode 100644 index 0000000..944dc21 --- /dev/null +++ b/src/components/DeprecatedTable/DeprecatedTable.js @@ -0,0 +1,103 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import { + Table, + TableBody, + TableHeader, +} from '@patternfly/react-table/deprecated'; +import { SkeletonTable } from '@patternfly/react-component-groups'; +import TableToolbar from '@redhat-cloud-services/frontend-components/TableToolbar'; + +import useTableToolsForDeprecatedTable from './hooks/useTableToolsForDeprecatedTable'; +import PrimaryToolbar from './PrimaryToolbar'; + +/** + * Deprecated PatternFly Table presentation variant. + * Owns PrimaryToolbar + table + footer TableToolbar. + * + * @param {object} props Component props (useTableTools output) + * @param {string} [props.view] Current table view + * @param {boolean} [props.loading] Loading state + * @param {object} [props.treeTable] Tree table config + * @param {Array} [props.columns] Column definitions + * @param {object} [props.toolbarProps] Toolbar props from useTableTools + * @param {object} [props.tableProps] PatternFly table props from useTableTools + * @param {object} [props.tableHeaderProps] Props for TableHeader + * @param {object} [props.tableBodyProps] Props for TableBody + * @param {object} [props.tableViewToggleProps] Props for TableViewToggle + * @param {object} [props.tableToolbarProps] Props for footer TableToolbar + * @param {object} [props.paginationProps] Props for bottom Pagination + * @returns {React.ReactElement} Deprecated table with toolbars + * + * @group Components + */ +const DeprecatedTable = (props) => { + const { + view, + loading, + toolbarProps, + tableProps, + treeTable, + columns, + tableHeaderProps, + tableBodyProps, + tableViewToggleProps, + } = useTableToolsForDeprecatedTable(props); + + const { tableToolbarProps, paginationProps } = props; + + return ( + <> + + + { + // TODO This is a bit hackish. We should rather have an indicator if data necessary for the current view is loading. + (view === 'rows' || (view === 'tree' && !treeTable)) && loading ? ( + title, + )} + /> + ) : ( + + + +
+ ) + } + + + {toolbarProps?.pagination && ( + + )} + + + ); +}; + +DeprecatedTable.propTypes = { + view: propTypes.string, + loading: propTypes.bool, + treeTable: propTypes.object, + columns: propTypes.array, + toolbarProps: propTypes.object, + tableProps: propTypes.object, + tableHeaderProps: propTypes.object, + tableBodyProps: propTypes.object, + tableViewToggleProps: propTypes.object, + tableToolbarProps: propTypes.object, + paginationProps: propTypes.object, +}; + +export default DeprecatedTable; diff --git a/src/components/TableToolsTable/TableToolsTable.stories.js b/src/components/DeprecatedTable/DeprecatedTable.stories.js similarity index 100% rename from src/components/TableToolsTable/TableToolsTable.stories.js rename to src/components/DeprecatedTable/DeprecatedTable.stories.js diff --git a/src/components/TableToolsTable/TableToolsTable.test.js b/src/components/DeprecatedTable/DeprecatedTable.test.js similarity index 85% rename from src/components/TableToolsTable/TableToolsTable.test.js rename to src/components/DeprecatedTable/DeprecatedTable.test.js index e99a61d..99b0adf 100644 --- a/src/components/TableToolsTable/TableToolsTable.test.js +++ b/src/components/DeprecatedTable/DeprecatedTable.test.js @@ -4,9 +4,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import items from '~/support/factories/items'; import TableStateProvider from '../TableStateProvider'; -import TableToolsTable from './TableToolsTable'; +import TableToolsTable from '../TableToolsTable'; -describe('TableToolsTable', () => { +describe('TableToolsTable (deprecated table variant)', () => { const exampleItems = items(100).sort((item) => item.name); const itemsFunc = jest.fn(async () => { return [exampleItems.slice(0, 10), exampleItems.length]; @@ -36,6 +36,4 @@ describe('TableToolsTable', () => { expect(screen.getByLabelText(ariaLabel)).toBeInTheDocument(); expect(await screen.findByText(exampleItems[1].title)).toBeInTheDocument(); }); - - // TODO Extend with more tests for basic filtering, paginating, sorting etc. }); diff --git a/src/components/TableToolsTable/TableToolsTableExperiments.stories.js b/src/components/DeprecatedTable/DeprecatedTableExperiments.stories.js similarity index 100% rename from src/components/TableToolsTable/TableToolsTableExperiments.stories.js rename to src/components/DeprecatedTable/DeprecatedTableExperiments.stories.js diff --git a/src/components/DeprecatedTable/PrimaryToolbar.js b/src/components/DeprecatedTable/PrimaryToolbar.js new file mode 100644 index 0000000..b7f7b5f --- /dev/null +++ b/src/components/DeprecatedTable/PrimaryToolbar.js @@ -0,0 +1,20 @@ +import React from 'react'; +import propTypes from 'prop-types'; + +import FECPrimaryToolbar from '@redhat-cloud-services/frontend-components/PrimaryToolbar'; + +import TableViewToggle from '../TableViewToggle'; + +const PrimaryToolbar = ({ toolbarProps, tableViewToggleProps }) => ( + + {toolbarProps?.children} + {tableViewToggleProps && } + +); + +PrimaryToolbar.propTypes = { + toolbarProps: propTypes.object, + tableViewToggleProps: propTypes.object, +}; + +export default PrimaryToolbar; diff --git a/src/components/DeprecatedTable/hooks/useTableToolsForDeprecatedTable.js b/src/components/DeprecatedTable/hooks/useTableToolsForDeprecatedTable.js new file mode 100644 index 0000000..7131314 --- /dev/null +++ b/src/components/DeprecatedTable/hooks/useTableToolsForDeprecatedTable.js @@ -0,0 +1,56 @@ +import { useMemo } from 'react'; + +/** + * Adapter: reshape useTableTools output for the deprecated PatternFly Table. + * + * @param {object} tableToolsProps Output from useTableTools (plus extras from parent) + * @param {string} [tableToolsProps.view] Current table view + * @param {boolean} [tableToolsProps.loading] Loading state + * @param {object} [tableToolsProps.toolbarProps] Toolbar props from useTableTools + * @param {object} [tableToolsProps.tableProps] PatternFly table props from useTableTools + * @param {object} [tableToolsProps.treeTable] Tree table config + * @param {Array} [tableToolsProps.columns] Column definitions + * @param {object} [tableToolsProps.tableHeaderProps] Props for TableHeader + * @param {object} [tableToolsProps.tableBodyProps] Props for TableBody + * @param {object} [tableToolsProps.tableViewToggleProps] Props for TableViewToggle + * @returns {object} Props ready for DeprecatedTable presentation + * + * @group Hooks + */ +const useTableToolsForDeprecatedTable = ({ + view, + loading, + toolbarProps, + tableProps, + treeTable, + columns, + tableHeaderProps, + tableBodyProps, + tableViewToggleProps, +}) => + useMemo( + () => ({ + view, + loading, + toolbarProps, + tableProps, + treeTable, + columns, + tableHeaderProps, + tableBodyProps, + tableViewToggleProps, + }), + [ + view, + loading, + toolbarProps, + tableProps, + treeTable, + columns, + tableHeaderProps, + tableBodyProps, + tableViewToggleProps, + ], + ); + +export default useTableToolsForDeprecatedTable; diff --git a/src/components/DeprecatedTable/index.js b/src/components/DeprecatedTable/index.js new file mode 100644 index 0000000..f2de238 --- /dev/null +++ b/src/components/DeprecatedTable/index.js @@ -0,0 +1 @@ +export { default } from './DeprecatedTable'; diff --git a/src/components/TableToolsTable/TableToolsTable.js b/src/components/TableToolsTable.js similarity index 67% rename from src/components/TableToolsTable/TableToolsTable.js rename to src/components/TableToolsTable.js index c4df589..d2d01cc 100644 --- a/src/components/TableToolsTable/TableToolsTable.js +++ b/src/components/TableToolsTable.js @@ -1,23 +1,15 @@ import React from 'react'; import propTypes from 'prop-types'; -import { Pagination, PaginationVariant } from '@patternfly/react-core'; -import { - Table, - TableBody, - TableHeader, -} from '@patternfly/react-table/deprecated'; -import { - SkeletonTable, - ColumnManagementModal, -} from '@patternfly/react-component-groups'; - -import PrimaryToolbar from '@redhat-cloud-services/frontend-components/PrimaryToolbar'; -import TableToolbar from '@redhat-cloud-services/frontend-components/TableToolbar'; +import { ColumnManagementModal } from '@patternfly/react-component-groups'; import useTableTools from '~/hooks/useTableTools'; -import { TableStateProvider, FilterModal, TableViewToggle } from '~/components'; + +import TableStateProvider from './TableStateProvider'; +import FilterModal from './FilterModal'; +import { variants } from './constants'; const TableToolsTable = ({ + tableToolsTableVariant = 'table', loading: externalLoading, items: externalItems, error: externalError, @@ -34,14 +26,12 @@ const TableToolsTable = ({ paginationProps, ...tablePropsRest }) => { + const TableComponent = variants[tableToolsTableVariant]; const { - view, - loading, toolbarProps, - tableProps, filterModalProps, columnManagerModalProps, - tableViewToggleProps, + ...tableToolsProps } = useTableTools( externalLoading, externalItems, @@ -59,37 +49,17 @@ const TableToolsTable = ({ return ( <> - - {toolbarProps?.children} - {tableViewToggleProps && } - - - { - // TODO This is a bit hackish. We should rather have an indicator if data necessary for the current view is loading. - (view === 'rows' || (view === 'tree' && !treeTable)) && loading ? ( - title)} - /> - ) : ( - - - -
- ) - } - - - {toolbarProps.pagination && ( - - )} - + {columnManagerModalProps && ( @@ -101,6 +71,7 @@ const TableToolsTable = ({ }; TableToolsTable.propTypes = { + tableToolsTableVariant: propTypes.string, items: propTypes.oneOfType([propTypes.array, propTypes.func]).isRequired, columns: propTypes.arrayOf( propTypes.shape({ @@ -142,7 +113,7 @@ TableToolsTable.propTypes = { * @param {object} [props.paginationProps] Props to be passed on the Pagination component * @returns {React.ReactElement} Returns a `PrimaryToolbar` component, a Patternfly (v4) `Table` component and a `TableToolbarComponent` wrapped together * - * @document ../../docs/using-table-tools.md + * @document ../docs/using-table-tools.md * * @group Components * diff --git a/src/components/TableToolsTable/index.js b/src/components/TableToolsTable/index.js deleted file mode 100644 index f3c11a2..0000000 --- a/src/components/TableToolsTable/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './TableToolsTable'; diff --git a/src/components/constants.js b/src/components/constants.js new file mode 100644 index 0000000..abcf92b --- /dev/null +++ b/src/components/constants.js @@ -0,0 +1,7 @@ +import DataViewTable from './DataViewTable'; +import DeprecatedTable from './DeprecatedTable'; + +export const variants = { + table: DeprecatedTable, + dataViewTable: DataViewTable, +}; diff --git a/src/components/index.js b/src/components/index.js index 9078b72..34695ee 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -5,3 +5,4 @@ export { default as NoResultsTable } from './NoResultsTable'; export { default as FilterModal } from './FilterModal'; export { default as TableViewToggle } from './TableViewToggle'; export { default as DataViewTable } from './DataViewTable'; +export { default as DeprecatedTable } from './DeprecatedTable';