diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js index 85482b432e..d9dedd85ac 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js @@ -9,10 +9,6 @@ import { ToolbarContent, ToolbarGroup, ToolbarItem, - PageSection, - PageSectionVariants, - TextContent, - Text, Icon, PaginationVariant, } from '@patternfly/react-core'; @@ -27,9 +23,8 @@ import { translate as __ } from '../../../common/I18n'; import { noop } from '../../../common/helpers'; import Pagination from '../../Pagination'; import { getControllerSearchProps, STATUS } from '../../../constants'; -import BreadcrumbBar from '../../BreadcrumbBar'; +import PageLayout from '../../../routes/common/PageLayout/PageLayout'; import SearchBar from '../../SearchBar'; -import Head from '../../Head'; import { ActionButtons } from './ActionButtons'; import './TableIndexPage.scss'; import { Table } from './Table/Table'; @@ -37,6 +32,7 @@ import { useSetParamsAndApiAndSearch, useTableIndexAPIResponse, } from './Table/TableIndexHooks'; + /** A page component that displays a table with data fetched from the API. It provides search and filtering functionality, and the ability to create new entries and export data. @@ -62,7 +58,6 @@ A page component that displays a table with data fetched from the API. It provid @param {boolean} {exportable} - whether or not to show export button @param {boolean} {hasHelpPage} - whether or not to show documentation button @param {React.ReactNode}{customHeader} - a custom header to be rendered instead of the default header -@param {string}{headerText} - DEPRECATED - the header text for the page @param {string}{header} -the header text for the page and the title @param {boolean} {isDeleteable} - whether or not entries can be deleted @param {boolean} {searchable} - whether or not the table can be searched @@ -101,7 +96,6 @@ const TableIndexPage = ({ exportable, hasHelpPage, customHeader, - headerText, header, isDeleteable, searchable, @@ -224,89 +218,66 @@ const TableIndexPage = ({ ...customActionButtons, ].filter(item => item); - header = headerText || header; - return ( -
- - {header} - - {breadcrumbOptions && ( - - - - )} - - {customHeader || ( - - - {header} - - - )} - - {beforeToolbarComponent} - - - - {searchable && ( - - {selectionToolbar} - - - - {status === STATUS.PENDING && ( - - - - )} - - )} - {(customToolbarItems || actionButtons.length > 0) && ( - - {actionButtons.length > 0 && ( - - - - )} - {customToolbarItems && customToolbarItems} - - )} - {total > 0 && ( - + + {searchable && ( + + {selectionToolbar} + + + + {status === STATUS.PENDING && ( + + + + )} + + )} + {(customToolbarItems || actionButtons.length > 0) && ( + + {actionButtons.length > 0 && ( + + + )} - - - - + )} + {total > 0 && ( + + )} + + + ); + + return ( +
+ {children || ( )} - + ); }; @@ -399,7 +370,6 @@ TableIndexPage.propTypes = { replacementResponse: PropTypes.object, exportable: PropTypes.bool, hasHelpPage: PropTypes.bool, - headerText: PropTypes.string, header: PropTypes.string, customHeader: PropTypes.node, isDeleteable: PropTypes.bool, @@ -442,7 +412,6 @@ TableIndexPage.defaultProps = { exportable: false, hasHelpPage: false, header: '', - headerText: '', customHeader: undefined, isDeleteable: false, searchable: true, diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss index cfc0317984..fd7cefb6b3 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss @@ -1,10 +1,8 @@ -#foreman-page { - .table-title-section { - padding-bottom: 0; - } - .table-toolbar-section { - padding-top: 16px; - padding-bottom: 16px; +.table-index-page { + .page-toolbar-section { + padding-top: var(--pf-v5-global--spacer--md); + padding-bottom: var(--pf-v5-global--spacer--md); + .table-toolbar { padding: 0; .pf-v5-c-toolbar__content { @@ -15,7 +13,7 @@ display: block; } .table-toolbar-actions { - padding-left: 16px; + padding-left: var(--pf-v5-global--spacer--md); } } .pf-v5-c-toolbar__group { @@ -25,7 +23,7 @@ } } } - .table-section { + .page-content-section { padding-top: 0; padding-left: 0; padding-right: 0; diff --git a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap index df284245f6..a5432cbc40 100644 --- a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap +++ b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap @@ -5,6 +5,8 @@ exports[`AuditsPage rendering render audits page 1`] = ` beforeToolbarComponent={null} breadcrumbOptions={null} customBreadcrumbs={null} + customHeader={null} + customToolbar={null} header="Audits" isLoading={false} onSearch={[Function]} @@ -79,6 +81,8 @@ exports[`AuditsPage rendering render audits page w/empty audits 1`] = ` beforeToolbarComponent={null} breadcrumbOptions={null} customBreadcrumbs={null} + customHeader={null} + customToolbar={null} header="Audits" isLoading={false} onSearch={[Function]} @@ -155,6 +159,8 @@ exports[`AuditsPage rendering render audits page w/error 1`] = ` beforeToolbarComponent={null} breadcrumbOptions={null} customBreadcrumbs={null} + customHeader={null} + customToolbar={null} header="Audits" isLoading={false} onSearch={[Function]} @@ -231,6 +237,8 @@ exports[`AuditsPage rendering render loading audits page 1`] = ` beforeToolbarComponent={null} breadcrumbOptions={null} customBreadcrumbs={null} + customHeader={null} + customToolbar={null} header="Audits" isLoading={false} onSearch={[Function]} diff --git a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js index 96f7046c3b..44f7bc8d34 100644 --- a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js +++ b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js @@ -24,13 +24,15 @@ const PageLayout = ({ customBreadcrumbs, breadcrumbOptions, toolbarButtons, + customToolbar, header, + customHeader, beforeToolbarComponent, isLoading, pageSectionType, children, }) => { - const title = ( + const titleSectionBody = customHeader ?? ( {header} @@ -38,6 +40,14 @@ const PageLayout = ({ ); + const showStandaloneTitleSection = searchable || !toolbarButtons; + + const toolbarSectionShowsDefaultToolbar = + !customToolbar && (searchable || isLoading || toolbarButtons); + + const showToolbarSection = + toolbarSectionShowsDefaultToolbar || Boolean(customToolbar); + return ( <> @@ -53,51 +63,55 @@ const PageLayout = ({ )} - {(searchable || !toolbarButtons) && ( + {showStandaloneTitleSection && ( - +
{titleSectionBody}
)} - {(searchable || - beforeToolbarComponent || - isLoading || - toolbarButtons) && ( + {beforeToolbarComponent} + + {showToolbarSection && ( - {beforeToolbarComponent} - - - - {!searchable && toolbarButtons && title} - {searchable && ( - - )} - - {isLoading && ( - - + {customToolbar || ( + + + + {!searchable && toolbarButtons && titleSectionBody} + {searchable && ( + + )} - )} - - {toolbarButtons} - - - + {isLoading && ( + + + + )} + + {toolbarButtons} + + + + )} )} - + {children} @@ -107,7 +121,6 @@ const PageLayout = ({ PageLayout.propTypes = { children: PropTypes.node.isRequired, searchable: PropTypes.bool.isRequired, - header: PropTypes.string, searchProps: PropTypes.shape({ autocomplete: PropTypes.shape({ results: PropTypes.array, @@ -147,22 +160,28 @@ PageLayout.propTypes = { ), }), toolbarButtons: PropTypes.node, + customToolbar: PropTypes.node, + header: PropTypes.string, + customHeader: PropTypes.node, onSearch: PropTypes.func, searchQuery: PropTypes.string, beforeToolbarComponent: PropTypes.node, isLoading: PropTypes.bool, pageSectionType: PropTypes.string, + className: PropTypes.string, }; PageLayout.defaultProps = { searchProps: {}, header: '', + customHeader: null, searchQuery: '', customBreadcrumbs: null, toolbarButtons: null, + customToolbar: null, breadcrumbOptions: null, isLoading: false, - onSearch: searchQuery => changeQuery({ search: searchQuery.trim(), page: 1 }), + onSearch: newSearch => changeQuery({ search: newSearch.trim(), page: 1 }), beforeToolbarComponent: null, pageSectionType: 'default', }; diff --git a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js index ca99fc63c1..b855d696df 100644 --- a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js +++ b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js @@ -4,6 +4,7 @@ import { screen } from '@testing-library/react'; import PageLayout from './PageLayout'; import '@testing-library/jest-dom'; +import { breadcrumbBar } from '../../../components/BreadcrumbBar/BreadcrumbBar.fixtures'; import { pageLayoutMock } from './PageLayout.fixtures'; import { rtlHelpers } from '../../../common/rtlTestHelpers'; @@ -11,7 +12,7 @@ const { renderWithStore } = rtlHelpers; jest.unmock('react-helmet'); describe('PageLayout', () => { - it('should render header text', () => { + it('renders string header as title when searchable is false (no SearchBar)', () => { const header = 'My Header'; const { getByText } = renderWithStore( @@ -30,7 +31,35 @@ describe('PageLayout', () => { expect(screen.queryAllByLabelText('Search')).toHaveLength(0); }); - it('should have Search', () => { + it('renders customHeader node inside title breadcrumb region', () => { + function CompositeHeader() { + return ( + + Scoped Hosts view + + ); + } + + renderWithStore( + + } + searchable={false} + > +
Content
+
+
+ ); + + const composite = screen.getByTestId('composite-header'); + expect(composite).toBeInTheDocument(); + expect(composite.closest('#page-title')).toBeInTheDocument(); + expect(screen.getByText('Hosts')).toBeInTheDocument(); + }); + + it('renders SearchBar in toolbar when searchable is true', () => { const onSearchMock = jest.fn(); const { getByLabelText } = renderWithStore( @@ -47,7 +76,7 @@ describe('PageLayout', () => { expect(getByLabelText('Search')).toBeInTheDocument(); }); - it('should render custom breadcrumbs', () => { + it('renders customBreadcrumbs when provided', () => { const customBreadcrumbs =
test Breadcrumbs
; const { getByText } = renderWithStore( @@ -64,7 +93,45 @@ describe('PageLayout', () => { expect(breadcrumbsElement).toBeInTheDocument(); }); - it('should render toolbar buttons', () => { + it('renders BreadcrumbBar from breadcrumbOptions', () => { + renderWithStore( + + +
Content
+
+
+ ); + expect(screen.getByText('root')).toBeInTheDocument(); + }); + + it('renders beforeToolbarComponent between title and toolbar sections', () => { + renderWithStore( + + TB} + beforeToolbarComponent={ +
Before toolbar
+ } + > +
Content
+
+
+ ); + expect(screen.getByTestId('before-toolbar')).toHaveTextContent( + 'Before toolbar' + ); + expect(screen.getByText('TB')).toBeInTheDocument(); + }); + + it('renders toolbarButtons in toolbar', () => { const toolbarButtons = ; const { getByText } = renderWithStore( @@ -81,7 +148,7 @@ describe('PageLayout', () => { expect(buttonElement).toBeInTheDocument(); }); - it('should render content', () => { + it('renders children in main content PageSection', () => { const { getByText } = renderWithStore( @@ -93,7 +160,7 @@ describe('PageLayout', () => { expect(contentElement).toBeInTheDocument(); }); - it('should show spinner when isLoading is true', () => { + it('shows toolbar spinner when isLoading is true', () => { const { container } = renderWithStore( @@ -104,7 +171,7 @@ describe('PageLayout', () => { expect(container.querySelector('#toolbar-spinner')).toBeInTheDocument(); }); - it('should not show spinner when isLoading is false', () => { + it('does not render toolbar spinner when isLoading is false', () => { const { container } = renderWithStore( @@ -114,4 +181,24 @@ describe('PageLayout', () => { ); expect(container.querySelector('#toolbar-spinner')).toBeNull(); }); + + it('renders customToolbar instead of built-in toolbar (skips SearchBar)', () => { + renderWithStore( + + Custom} + header="Index" + > +
Content
+
+
+ ); + expect(screen.getByTestId('custom-toolbar-slot')).toHaveTextContent( + 'Custom' + ); + expect(screen.queryByLabelText('Search input')).not.toBeInTheDocument(); + }); });