Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add AdminHeader component wrapping @wordpress/admin-ui Page for unified admin page headers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* Proxy file to import @wordpress/admin-ui styles.
*
* Importing the CSS via @import inside a .css file avoids the
* @wordpress/dependency-extraction-webpack-plugin externalization that
* breaks direct JS `import '@wordpress/admin-ui/build-style/style.css'`.
* css-loader resolves @import independently of the externals plugin.
*/
@import "@wordpress/admin-ui/build-style/style.css";
59 changes: 59 additions & 0 deletions projects/js-packages/components/components/admin-header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Page } from '@wordpress/admin-ui';
import './admin-ui-styles.css';
import {
__experimentalHeading as Heading, // eslint-disable-line @wordpress/no-unsafe-wp-apis
__experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
} from '@wordpress/components';
import JetpackLogo from '../jetpack-logo/index.tsx';
import type { AdminHeaderProps } from './types.ts';
import type { FC } from 'react';

/**
* Unified admin page header component.
*
* Renders a sticky header with logo, product title, optional subtitle,
* actions, and tabs. Wraps the `@wordpress/admin-ui` Page component.
*
* @param {AdminHeaderProps} props - Component properties.
* @return {ReactNode} AdminHeader component.
*/
const AdminHeader: FC< AdminHeaderProps > = ( {
logo,
title,
subTitle,
actions,
tabs = null,
className,
breadcrumbs = null,
badges = null,
} ) => {
const classes = className;

// While admin-ui Page has a title prop, it fails to render both the logo and
// text. Internally it tries to accommodate both inside Heading.
// Composing here with Heading as it is on admin-ui Page.
const composedTitle = title ? (
<HStack spacing={ 2 } justify="left">
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HStack uses justify="left", but the codebase more commonly uses justify="start" for left alignment (see examples in forms/routes/responses/integrations-modal.tsx:184, forms/src/blocks/contact-form/components/jetpack-integrations-modal/helpers/akismet.tsx:76, and many others). While both work, using justify="start" would be more consistent with the rest of the codebase.

Suggested change
<HStack spacing={ 2 } justify="left">
<HStack spacing={ 2 } justify="start">

Copilot uses AI. Check for mistakes.
{ logo || <JetpackLogo showText={ false } height={ 20 } /> }
<Heading as="h2" level={ 3 } weight={ 500 } truncate>
{ title }
</Heading>
</HStack>
) : undefined;

return (
<Page
className={ classes }
title={ composedTitle }
subTitle={ subTitle }
actions={ actions }
breadcrumbs={ breadcrumbs }
badges={ badges }
showSidebarToggle={ false }
>
{ tabs }
</Page>
);
Comment on lines +44 to +56
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @wordpress/admin-ui Page component is designed to wrap the entire page content (header + body), as seen in its usage throughout the forms package. However, AdminHeader uses Page only for the header portion, with only tabs passed as children (line 53), while the actual page content is rendered separately in AdminPage (line 102). This creates an unusual architecture where Page doesn't contain the page content. Consider either: (1) refactoring to have Page wrap all content, or (2) using a more direct approach to render just the header elements without wrapping them in the full Page component. The current approach may cause issues if @wordpress/admin-ui expects Page to manage layout for its entire content area.

Copilot uses AI. Check for mistakes.
};

export default AdminHeader;
Comment on lines +20 to +59
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new AdminHeader component is introduced without any test coverage. While the old AdminPageHeader test was deleted (which is appropriate since that component was removed), the new component should have tests to verify its prop handling, logo defaulting behavior, and integration with the @wordpress/admin-ui Page component. Consider adding tests for AdminHeader similar to the test coverage that existed for the old AdminPageHeader.

Copilot uses AI. Check for mistakes.
43 changes: 43 additions & 0 deletions projects/js-packages/components/components/admin-header/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ReactNode } from 'react';

export type AdminHeaderProps = {
/**
* Custom logo element. Defaults to JetpackLogo icon (bolt only).
*/
logo?: ReactNode;

/**
* Product title displayed next to the logo.
*/
title: string;

/**
* Optional subtitle displayed below the title row.
*/
subTitle?: string;

/**
* Optional breadcrumb elements displayed next to the title.
*/
breadcrumbs?: ReactNode;

/**
* Optional badge elements displayed next to the title.
*/
badges?: ReactNode;

/**
* Optional action elements (buttons, links) displayed on the right side of the header.
*/
actions?: ReactNode;

/**
* Optional tab navigation displayed below the title/tagline.
*/
tabs?: ReactNode;

/**
* Additional CSS class name.
*/
className?: string;
};
54 changes: 35 additions & 19 deletions projects/js-packages/components/components/admin-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import restApi from '@automattic/jetpack-api';
import { __, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import { useEffect, useCallback } from 'react';
import AdminHeader from '../admin-header/index.tsx';
import JetpackFooter from '../jetpack-footer/index.tsx';
import JetpackLogo from '../jetpack-logo/index.tsx';
import Col from '../layout/col/index.tsx';
Expand Down Expand Up @@ -32,6 +33,11 @@ const AdminPage: FC< AdminPageProps > = ( {
apiNonce = '',
optionalMenuItems,
header,
title,
subTitle,
logo,
actions,
tabs,
} ) => {
useEffect( () => {
restApi.setApiRoot( apiRoot );
Expand Down Expand Up @@ -62,25 +68,35 @@ const AdminPage: FC< AdminPageProps > = ( {

return (
<div className={ rootClassName }>
{ showHeader && (
<Container horizontalSpacing={ 5 }>
<Col className={ clsx( styles[ 'admin-page-header' ], 'jp-admin-page-header' ) }>
{ header ? header : <JetpackLogo /> }
{ sandboxedDomain && (
<code
className={ styles[ 'sandbox-domain-badge' ] }
onClick={ testConnection }
onKeyDown={ testConnection }
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={ 0 }
title={ `Sandboxing via ${ sandboxedDomain }. Click to test connection.` }
>
API Sandboxed
</code>
) }
</Col>
</Container>
{ showHeader && title ? (
<AdminHeader
logo={ logo }
title={ title }
subTitle={ subTitle }
actions={ actions }
tabs={ tabs }
/>
) : (
showHeader && (
<Container horizontalSpacing={ 5 }>
<Col className={ clsx( styles[ 'admin-page-header' ], 'jp-admin-page-header' ) }>
{ header ? header : <JetpackLogo /> }
{ sandboxedDomain && (
<code
className={ styles[ 'sandbox-domain-badge' ] }
onClick={ testConnection }
onKeyDown={ testConnection }
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="button"
tabIndex={ 0 }
title={ `Sandboxing via ${ sandboxedDomain }. Click to test connection.` }
>
API Sandboxed
</code>
) }
</Col>
</Container>
)
) }
<Container fluid horizontalSpacing={ 0 }>
<Col>{ children }</Col>
Expand Down
29 changes: 28 additions & 1 deletion projects/js-packages/components/components/admin-page/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,37 @@ export type AdminPageProps = {
showHeader?: boolean;

/**
* Custom header. Optional
* Custom header. Optional.
* @deprecated Use `title` and `subTitle` props instead for the unified header.
*/
header?: ReactNode;

/**
* Product title displayed in the unified header (e.g. "Social", "Backup").
* When provided, renders the new AdminHeader instead of the legacy header slot.
*/
title?: string;

/**
* Optional tagline displayed below the title in the unified header.
*/
subTitle?: string;

/**
* Custom logo element for the unified header. Defaults to JetpackLogo icon.
*/
logo?: ReactNode;

/**
* Action elements displayed on the right side of the unified header.
*/
actions?: ReactNode;

/**
* Tab navigation displayed below the title/tagline in the unified header.
*/
tabs?: ReactNode;
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states that AdminPage props are extended with breadcrumbs and badges, but these props are not defined in AdminPageProps nor are they being passed through to AdminHeader. If these props are intended to be part of the public API, they should be added to AdminPageProps and passed to AdminHeader at lines 72-78. If they were mentioned in error, the PR description should be updated.

Copilot uses AI. Check for mistakes.

/**
* Whether or not to display the Footer
*/
Expand Down
2 changes: 2 additions & 0 deletions projects/js-packages/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export { default as NumberSlider } from './components/number-slider/index.tsx';
export { default as AdminSection } from './components/admin-section/basic/index.tsx';
export { default as AdminSectionHero } from './components/admin-section/hero/index.tsx';
export { default as AdminPage } from './components/admin-page/index.tsx';
export { default as AdminHeader } from './components/admin-header/index.tsx';
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider exporting the AdminHeaderProps type from the main index.ts for consumers who need to reference these types in their TypeScript code. This follows the pattern established for other components like ButtonProps (line 52 in index.ts).

Suggested change
export { default as AdminHeader } from './components/admin-header/index.tsx';
export { default as AdminHeader } from './components/admin-header/index.tsx';
export type { AdminHeaderProps } from './components/admin-header/types.ts';

Copilot uses AI. Check for mistakes.
export type { AdminHeaderProps } from './components/admin-header/types.ts';
export { default as DecorativeCard } from './components/decorative-card/index.tsx';
export { default as Col } from './components/layout/col/index.tsx';
export { default as Testimonials } from './components/testimonials/index.tsx';
Expand Down
1 change: 1 addition & 0 deletions projects/js-packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@automattic/jetpack-script-data": "workspace:*",
"@automattic/number-formatters": "workspace:*",
"@babel/runtime": "^7",
"@wordpress/admin-ui": "1.8.0",
"@wordpress/browserslist-config": "6.40.0",
"@wordpress/components": "32.2.0",
"@wordpress/compose": "7.40.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function main() {
return;
}

for await ( const filePath of fs.glob( '**/*.scss', {
for await ( const filePath of fs.glob( '**/*.{scss,css}', {
cwd: packageRoot,
exclude: Array.from( IGNORED_DIRS, dir => `**/${ dir }/**` ),
} ) ) {
Expand Down
25 changes: 21 additions & 4 deletions projects/packages/publicize/_inc/components/admin-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ import {
} from '@automattic/jetpack-components';
import { useConnection } from '@automattic/jetpack-connection';
import {
getMyJetpackUrl,
isJetpackSelfHostedSite,
isSimpleSite,
siteHasFeature,
currentUserCan,
} from '@automattic/jetpack-script-data';
import { shouldUseInternalLinks } from '@automattic/jetpack-shared-extension-utils';
import { useSelect } from '@wordpress/data';
import { useState, useCallback } from '@wordpress/element';
import { createInterpolateElement, useState, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { store as socialStore } from '../../social-store';
import { features, getSocialScriptData, hasSocialPaidFeatures } from '../../utils';
import ConnectionScreen from './connection-screen';
import Header from './header';
import InfoSection from './info-section';
import AdminPageHeader from './page-header';
import './styles.module.scss';
import PricingPage from './pricing-page';
import SupportSection from './support-section';
Expand Down Expand Up @@ -65,7 +66,8 @@ export const SocialAdminPage = () => {
return (
<AdminPage
moduleName={ moduleName }
showHeader={ false }
title={ __( 'Social', 'jetpack-publicize-pkg' ) }
subTitle={ __( 'Publish once. Share everywhere.', 'jetpack-publicize-pkg' ) }
showBackground={ false }
useInternalLinks={ shouldUseInternalLinks() }
>
Expand All @@ -78,10 +80,25 @@ export const SocialAdminPage = () => {
);
}

const licenseAction =
! hasSocialPaidFeatures() && isJetpackSite
? createInterpolateElement(
__(
'Already have an existing plan or license key? <a>Click here to get started</a>',
'jetpack-publicize-pkg'
),
{
a: <a href={ getMyJetpackUrl( '#/add-license' ) } />,
}
)
: null;
Comment on lines +83 to +94
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleted test file page-header.test.jsx contained tests for the license text functionality (showing/hiding based on paid features and site type). These tests should be migrated to ensure the license action behavior is still covered. Consider adding test cases to social-admin-page.test.jsx to verify that the license action appears in the header's actions prop when appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +94
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleted page-header.test.jsx file contained tests verifying that the license action text ("Already have an existing plan or license key?") appears conditionally based on whether the site has paid features and whether it's a Jetpack site. This behavior is now implemented in the licenseAction variable (lines 83-94) and passed as the actions prop, but there's no test coverage for this functionality in the remaining test files. Consider adding tests to social-admin-page.test.jsx to verify the license action appears/disappears under the correct conditions.

Copilot uses AI. Check for mistakes.

return (
<AdminPage
moduleName={ moduleName }
header={ <AdminPageHeader /> }
title={ __( 'Social', 'jetpack-publicize-pkg' ) }
subTitle={ __( 'Publish once. Share everywhere.', 'jetpack-publicize-pkg' ) }
actions={ licenseAction }
showFooter={ isJetpackSite }
useInternalLinks={ shouldUseInternalLinks() }
>
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading