diff --git a/cypress/component/DataViewTableBasic.cy.tsx b/cypress/component/DataViewTableBasic.cy.tsx index e70df57..cc2f9d7 100644 --- a/cypress/component/DataViewTableBasic.cy.tsx +++ b/cypress/component/DataViewTableBasic.cy.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import DataViewTableBasic from '@patternfly/react-data-view/dist/esm/DataViewTableBasic'; +import DataViewTableBasic from '@patternfly/react-data-view/dist/dynamic/DataViewTableBasic'; +import DataView from '@patternfly/react-data-view/dist/dynamic/DataView'; interface Repository { name: string; @@ -48,7 +49,9 @@ describe('DataViewTableBasic', () => { const ouiaId = 'data'; cy.mount( - + + + ); cy.get('[data-ouia-component-id="data-th-0"]').contains('Repositories'); @@ -60,4 +63,23 @@ describe('DataViewTableBasic', () => { cy.get('[data-ouia-component-id="data-tr-empty"]').should('be.visible'); cy.get('[data-ouia-component-id="data-tr-empty"]').contains('No data found'); }); + + it('renders a basic data view table with an error state', () => { + const ouiaId = 'data'; + + cy.mount( + + + + ); + + cy.get('[data-ouia-component-id="data-th-0"]').contains('Repositories'); + cy.get('[data-ouia-component-id="data-th-1"]').contains('Branches'); + cy.get('[data-ouia-component-id="data-th-2"]').contains('Pull requests'); + cy.get('[data-ouia-component-id="data-th-3"]').contains('Workspaces'); + cy.get('[data-ouia-component-id="data-th-4"]').contains('Last commit'); + + cy.get('[data-ouia-component-id="data-tr-error"]').should('be.visible'); + cy.get('[data-ouia-component-id="data-tr-error"]').contains('Some error'); + }); }); \ No newline at end of file diff --git a/cypress/component/DataViewTableTree.cy.tsx b/cypress/component/DataViewTableTree.cy.tsx index 7d83e37..32c4b52 100644 --- a/cypress/component/DataViewTableTree.cy.tsx +++ b/cypress/component/DataViewTableTree.cy.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { DataViewTable, DataViewTrTree } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import DataView from '@patternfly/react-data-view/dist/dynamic/DataView'; interface Repository { name: string; @@ -130,7 +131,9 @@ describe('DataViewTableTree', () => { const ouiaId = 'tree'; cy.mount( - + + + ); cy.get('[data-ouia-component-id="tree-th-0"]').contains('Repositories'); @@ -142,4 +145,23 @@ describe('DataViewTableTree', () => { cy.get('[data-ouia-component-id="tree-tr-empty"]').should('be.visible'); cy.get('[data-ouia-component-id="tree-tr-empty"]').contains('No data found'); }); + + it('renders a tree data view table with an error state', () => { + const ouiaId = 'data'; + + cy.mount( + + + + ); + + cy.get('[data-ouia-component-id="data-th-0"]').contains('Repositories'); + cy.get('[data-ouia-component-id="data-th-1"]').contains('Branches'); + cy.get('[data-ouia-component-id="data-th-2"]').contains('Pull requests'); + cy.get('[data-ouia-component-id="data-th-3"]').contains('Workspaces'); + cy.get('[data-ouia-component-id="data-th-4"]').contains('Last commit'); + + cy.get('[data-ouia-component-id="data-tr-error"]').should('be.visible'); + cy.get('[data-ouia-component-id="data-tr-error"]').contains('Some error'); + }); }); \ No newline at end of file diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md index bc2a0a0..cde85a5 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/Components.md @@ -16,11 +16,11 @@ sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/mod --- import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon } from '@patternfly/react-core'; import { CubesIcon, FolderIcon, FolderOpenIcon, LeafIcon, ExclamationCircleIcon } from '@patternfly/react-icons'; -import { BulkSelect } from '@patternfly/react-component-groups'; +import { BulkSelect, ErrorState } from '@patternfly/react-component-groups'; import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; import { useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks'; -import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; +import { DataView, DataViewState } from '@patternfly/react-data-view/dist/dynamic/DataView'; ## Data view toolbar @@ -74,8 +74,15 @@ It is also possible to disable row selection using the `isSelectDisabled` functi ``` ### Empty state example -The data view table also supports displaying a custom empty state. You can pass it using the `emptyState` property and it will be displayed in case there are no rows to be rendered. +The data view table supports displaying a custom empty state. You can pass it using the `states` property and `empty` key. It will be automatically displayed in case there are no rows to be rendered. ```js file="./DataViewTableEmptyExample.tsx" ``` + +### Error state example +The data view table also supports displaying an error state. You can pass it using the `states` property and `error` key. It will be displayed in case the data view recieves its `state` property set to `error`. + +```js file="./DataViewTableErrorExample.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableEmptyExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableEmptyExample.tsx index 9819a8b..bda011f 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableEmptyExample.tsx +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableEmptyExample.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DataView, DataViewState } from '@patternfly/react-data-view/dist/dynamic/DataView'; import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; import { CubesIcon } from '@patternfly/react-icons'; import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon } from '@patternfly/react-core'; @@ -21,7 +22,7 @@ const columns: DataViewTh[] = [ 'Repositories', 'Branches', 'Pull requests', 'Wo const ouiaId = 'TableExample'; -const emptyState = ( +const empty = ( } /> There are no matching data to be displayed. @@ -38,11 +39,13 @@ const emptyState = ( ); export const BasicExample: React.FunctionComponent = () => ( - + + + ); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableErrorExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableErrorExample.tsx new file mode 100644 index 0000000..1285575 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableErrorExample.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { DataView, DataViewState } from '@patternfly/react-data-view/dist/dynamic/DataView'; +import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { ErrorState } from '@patternfly/react-component-groups'; + +interface Repository { + id: number; + name: string; + branches: string | null; + prs: string | null; + workspaces: string; + lastCommit: string; +} + +const repositories: Repository[] = []; + +// you can also pass props to Tr by returning { row: DataViewTd[], props: TrProps } } +const rows: DataViewTr[] = repositories.map((repository) => Object.values(repository)); + +const columns: DataViewTh[] = [ 'Repositories', 'Branches', 'Pull requests', 'Workspaces', 'Last commit' ]; + +const ouiaId = 'TableErrorExample'; + +const error = ( + +); + +export const BasicExample: React.FunctionComponent = () => ( + + + +); diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md index f984daa..13c892d 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md @@ -11,7 +11,7 @@ source: react # If you use typescript, the name of the interface to display props for # These are found through the sourceProps function provided in patternfly-docs.source.js sortValue: 2 -propComponents: ['DataView'] +propComponents: ['DataView', 'DataViewState'] sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Layout/Layout.md --- import { useMemo } from 'react'; diff --git a/packages/module/src/DataView/DataView.tsx b/packages/module/src/DataView/DataView.tsx index b2a0de6..cf0fdcb 100644 --- a/packages/module/src/DataView/DataView.tsx +++ b/packages/module/src/DataView/DataView.tsx @@ -2,13 +2,23 @@ import React from 'react'; import { Stack, StackItem } from '@patternfly/react-core'; import { DataViewSelection, InternalContextProvider } from '../InternalContext'; +export const DataViewState = { + empty: 'empty', + loading: 'loading', + error: 'error' +} as const; + +export type DataViewState = typeof DataViewState[keyof typeof DataViewState]; + export interface DataViewProps { /** Content rendered inside the data view */ children: React.ReactNode; /** Custom OUIA ID */ ouiaId?: string; /** Selection context configuration */ - selection?: DataViewSelection + selection?: DataViewSelection; + /** Currently active state */ + activeState?: DataViewState; } export type DataViewImpementationProps = Omit; @@ -16,7 +26,7 @@ export type DataViewImpementationProps = Omit = ({ children, ouiaId = 'DataView', ...props }: DataViewImpementationProps) => ( - + {React.Children.map(children, (child, index) => ( {child} @@ -25,8 +35,8 @@ const DataViewImplementation: React.FC = ({ ) -export const DataView: React.FC = ({ children, selection, ...props }: DataViewProps) => ( - +export const DataView: React.FC = ({ children, selection, activeState, ...props }: DataViewProps) => ( + {children} ); diff --git a/packages/module/src/DataView/__snapshots__/DataView.test.tsx.snap b/packages/module/src/DataView/__snapshots__/DataView.test.tsx.snap index 1309d01..1ee3962 100644 --- a/packages/module/src/DataView/__snapshots__/DataView.test.tsx.snap +++ b/packages/module/src/DataView/__snapshots__/DataView.test.tsx.snap @@ -7,7 +7,7 @@ exports[`DataView component should render correctly 1`] = `
{ test('should render with an empty state', () => { const { container } = render( - + + + + ); + expect(container).toMatchSnapshot(); + }); + + test('should render with an error state', () => { + const { container } = render( + + + ); expect(container).toMatchSnapshot(); }); diff --git a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx index 5f8860b..1bf57ae 100644 --- a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx +++ b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx @@ -9,14 +9,15 @@ import { import { useInternalContext } from '../InternalContext'; import { DataViewTableHeader } from '../DataViewTableHeader'; import { DataViewTh, DataViewTr, isDataViewTdObject, isDataViewTrObject } from '../DataViewTable'; +import { DataViewState } from '../DataView/DataView'; export interface DataViewTableBasicProps extends Omit { /** Columns definition */ columns: DataViewTh[]; /** Current page rows */ rows: DataViewTr[]; - /** Empty state to be displayed */ - emptyState?: React.ReactNode; + /** States to be displayed when active */ + states?: Partial> /** Custom OUIA ID */ ouiaId?: string; } @@ -25,54 +26,56 @@ export const DataViewTableBasic: React.FC = ({ columns, rows, ouiaId = 'DataViewTableBasic', - emptyState = null, + states = {}, ...props }: DataViewTableBasicProps) => { - const { selection } = useInternalContext(); + const { selection, activeState } = useInternalContext(); const { onSelect, isSelected, isSelectDisabled } = selection ?? {}; - const isSelectable = useMemo(() => Boolean(onSelect && isSelected), [ onSelect, isSelected ]) + const isSelectable = useMemo(() => Boolean(onSelect && isSelected), [ onSelect, isSelected ]); return ( - {rows?.length > 0 ? rows.map((row, rowIndex) => { - const rowIsObject = isDataViewTrObject(row); - return ( - - {isSelectable && ( - - ) - })} - - )}) : ( - + {activeState && Object.keys(states).includes(activeState) ? ( + - )} + ) : ( + rows.map((row, rowIndex) => { + const rowIsObject = isDataViewTrObject(row); + 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 ( - - {cellIsObject ? cell.cell : cell} -
- {emptyState} + {states[activeState]}
{ + 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 ( + + {cellIsObject ? cell.cell : cell} +
); diff --git a/packages/module/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap b/packages/module/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap index 721ae6c..e5cca9c 100644 --- a/packages/module/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap +++ b/packages/module/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap @@ -325,85 +325,190 @@ exports[`DataViewTable component should render correctly 1`] = ` exports[`DataViewTable component should render with an empty state 1`] = `
- - - - - - - - - - - + + + + + + + + + + + + + +
- Repositories - - Branches - - Pull requests - - Workspaces - - Last commit -
+ Repositories + + Branches + + Pull requests + + Workspaces + + Last commit +
+ No data found +
+
+
+
+`; + +exports[`DataViewTable component should render with an error state 1`] = ` +
+
+
- - - No data found - - - - + + + + Repositories + + + Branches + + + Pull requests + + + Workspaces + + + Last commit + + + + + + + Some error + + + + +
+
`; diff --git a/packages/module/src/DataViewTableHeader/__snapshots__/DataViewTableHeader.test.tsx.snap b/packages/module/src/DataViewTableHeader/__snapshots__/DataViewTableHeader.test.tsx.snap index 5935895..cf104c1 100644 --- a/packages/module/src/DataViewTableHeader/__snapshots__/DataViewTableHeader.test.tsx.snap +++ b/packages/module/src/DataViewTableHeader/__snapshots__/DataViewTableHeader.test.tsx.snap @@ -69,7 +69,7 @@ exports[`DataViewTableHeader component should render selection column when selec
{ test('should render tree table with an empty state', () => { const { container } = render( - + + + + + ); + expect(container).toMatchSnapshot(); + }); + + test('should render tree table with an error state', () => { + const { container } = render( + + + ); expect(container).toMatchSnapshot(); }); diff --git a/packages/module/src/DataViewTableTree/DataViewTableTree.tsx b/packages/module/src/DataViewTableTree/DataViewTableTree.tsx index ce3cb65..9ec262c 100644 --- a/packages/module/src/DataViewTableTree/DataViewTableTree.tsx +++ b/packages/module/src/DataViewTableTree/DataViewTableTree.tsx @@ -11,6 +11,7 @@ import { import { useInternalContext } from '../InternalContext'; import { DataViewTableHeader } from '../DataViewTableHeader'; import { DataViewTh, DataViewTrTree, isDataViewTdObject } from '../DataViewTable'; +import { DataViewState } from '../DataView/DataView'; const getDescendants = (node: DataViewTrTree): DataViewTrTree[] => (!node.children || !node.children.length) ? [ node ] : node.children.flatMap(getDescendants); @@ -35,8 +36,8 @@ export interface DataViewTableTreeProps extends Omit> /** Optional icon for the leaf rows */ leafIcon?: React.ReactNode; /** Optional icon for the expanded parent rows */ @@ -50,20 +51,20 @@ export interface DataViewTableTreeProps extends Omit = ({ columns, rows, - emptyState = null, + states = {}, leafIcon = null, expandedIcon = null, collapsedIcon = null, ouiaId = 'DataViewTableTree', ...props }: DataViewTableTreeProps) => { - const { selection } = useInternalContext(); + const { selection, activeState } = useInternalContext(); const { onSelect, isSelected, isSelectDisabled } = selection ?? {}; const [ expandedNodeIds, setExpandedNodeIds ] = React.useState([]); const [ expandedDetailsNodeNames, setExpandedDetailsNodeIds ] = React.useState([]); const nodes = useMemo(() => { - + const renderRows = ( [ node, ...remainingNodes ]: DataViewTrTree[], level = 1, @@ -134,19 +135,31 @@ export const DataViewTableTree: React.FC = ({ }; return renderRows(rows); - }, [ rows, expandedNodeIds, expandedDetailsNodeNames, leafIcon, expandedIcon, collapsedIcon, isSelected, onSelect, isSelectDisabled, ouiaId ]); + }, [ + rows, + expandedNodeIds, + expandedDetailsNodeNames, + leafIcon, + expandedIcon, + collapsedIcon, + isSelected, + onSelect, + isSelectDisabled, + ouiaId + ]); return ( - - {nodes.length > 0 ? nodes : ( - + { + activeState && Object.keys(states).includes(activeState) ? ( + - )} + ) : nodes + }
- {emptyState} + {states[activeState]}
); diff --git a/packages/module/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap b/packages/module/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap index ba5cc73..739aab8 100644 --- a/packages/module/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap +++ b/packages/module/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap @@ -4,7 +4,7 @@ exports[`DataViewTableTree component should render the tree table correctly 1`]
- - - - - - - - + + + + + + + - Last commit - - - - + + + +
- Repositories - - Branches - - Pull requests - - Workspaces - + + Repositories + + Branches + + Pull requests + + Workspaces + + Last commit +
+ No data found +
+
+
+
+`; + +exports[`DataViewTableTree component should render tree table with an error state 1`] = ` +
+
+
- - + + + Repositories + + + Branches + + + Pull requests + + + Workspaces + + + Last commit + + + + - No data found - - - - + + + Some error + + + + +
+
`; diff --git a/packages/module/src/InternalContext/InternalContext.tsx b/packages/module/src/InternalContext/InternalContext.tsx index 4f7e2b2..6e8be00 100644 --- a/packages/module/src/InternalContext/InternalContext.tsx +++ b/packages/module/src/InternalContext/InternalContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, PropsWithChildren, useContext } from 'react'; +import { DataViewState } from '../DataView'; export interface DataViewSelection { /** Called when the selection of items changes */ @@ -11,20 +12,23 @@ export interface DataViewSelection { export interface InternalContextValue { selection?: DataViewSelection; + activeState?: DataViewState; } export const InternalContext = createContext({ - selection: undefined + selection: undefined, + activeState: undefined }); export type InternalProviderProps = PropsWithChildren export const InternalContextProvider: React.FC = ({ children, - selection + selection, + activeState }) => ( {children}