Merge pull request #28876 from johnphilip283/update-techdocs-page-title

Update techdocs page title
This commit is contained in:
John Philip
2025-02-18 11:13:02 -05:00
committed by GitHub
4 changed files with 111 additions and 9 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Adds the page name of techdoc to the document's title.
@@ -17,7 +17,10 @@
import React from 'react';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import {
entityPresentationApiRef,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import {
mockApis,
renderInTestApp,
@@ -81,6 +84,16 @@ const techdocsStorageApiMock: jest.Mocked<typeof techdocsStorageApiRef.T> = {
syncEntityDocs: jest.fn(),
};
const entityPresentationApiMock: jest.Mocked<
typeof entityPresentationApiRef.T
> = {
forEntity: jest.fn().mockReturnValue({
snapshot: {
primaryTitle: 'Test Entity',
},
}),
};
const fetchApiMock = {
fetch: jest.fn().mockResolvedValue({
ok: true,
@@ -115,6 +128,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => {
[configApiRef, configApi],
[techdocsApiRef, techdocsApiMock],
[techdocsStorageApiRef, techdocsStorageApiMock],
[entityPresentationApiRef, entityPresentationApiMock],
]}
>
{children}
@@ -16,7 +16,10 @@
import React from 'react';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import {
entityPresentationApiRef,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import {
techdocsApiRef,
TechDocsReaderPageProvider,
@@ -26,6 +29,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { rootRouteRef } from '../../../routes';
import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
import { waitFor } from '@testing-library/react';
const mockEntityMetadata = {
locationMetadata: {
@@ -48,6 +52,16 @@ const mockTechDocsMetadata = {
site_description: 'test-site-desc',
};
const mockUseParams = jest.fn();
mockUseParams.mockReturnValue({ '*': 'foo/bar/baz/' });
jest.mock('react-router-dom', () => {
return {
...(jest.requireActual('react-router-dom') as any),
useParams: () => mockUseParams(),
};
});
const getEntityMetadata = jest.fn();
const getTechDocsMetadata = jest.fn();
@@ -56,6 +70,18 @@ const techdocsApiMock = {
getTechDocsMetadata,
};
const forEntity = jest.fn();
forEntity.mockReturnValue({
snapshot: {
primaryTitle: 'Test Entity',
},
});
const entityPresentationApiMock = {
forEntity,
};
const Wrapper = ({
entityRef = {
kind: mockEntityMetadata.kind,
@@ -67,7 +93,12 @@ const Wrapper = ({
entityRef?: CompoundEntityRef;
children: React.ReactNode;
}) => (
<TestApiProvider apis={[[techdocsApiRef, techdocsApiMock]]}>
<TestApiProvider
apis={[
[techdocsApiRef, techdocsApiMock],
[entityPresentationApiRef, entityPresentationApiMock],
]}
>
<TechDocsReaderPageProvider entityRef={entityRef}>
{children}
</TechDocsReaderPageProvider>
@@ -96,9 +127,10 @@ describe('<TechDocsReaderPageHeader />', () => {
expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
expect(rendered.getByText('test-site-desc')).toBeDefined();
expect(
rendered.getByRole('link', { name: 'test:test-namespace/test-name' }),
).toHaveAttribute('href', '/catalog/test-namespace/test/test-name');
expect(rendered.getByRole('link', { name: 'Test Entity' })).toHaveAttribute(
'href',
'/catalog/test-namespace/test/test-name',
);
});
it('should render a techdocs page header even if metadata is not loaded', async () => {
@@ -152,4 +184,27 @@ describe('<TechDocsReaderPageHeader />', () => {
expect(rendered.container.innerHTML).not.toContain('header');
});
it('The header title changes depending on the url params', async () => {
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
await renderInTestApp(
<Wrapper>
<TechDocsReaderPageHeader />
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
'/docs': rootRouteRef,
},
},
);
await waitFor(() => {
expect(document.title).toEqual(
'Backstage | Test Entity | Foo | Bar | Baz',
);
});
});
});
@@ -29,17 +29,23 @@ import {
TechDocsMetadata,
} from '@backstage/plugin-techdocs-react';
import {
entityPresentationApiRef,
EntityRefLink,
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { RELATION_OWNED_BY, CompoundEntityRef } from '@backstage/catalog-model';
import {
RELATION_OWNED_BY,
CompoundEntityRef,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Header, HeaderLabel } from '@backstage/core-components';
import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api';
import { capitalize } from 'lodash';
import capitalize from 'lodash/capitalize';
import { rootRouteRef } from '../../../routes';
import { useParams } from 'react-router-dom';
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
@@ -68,6 +74,9 @@ export const TechDocsReaderPageHeader = (
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const entityPresentationApi = useApi(entityPresentationApiRef);
const { '*': path = '' } = useParams();
const {
title,
setTitle,
@@ -91,7 +100,6 @@ export const TechDocsReaderPageHeader = (
}, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [title, subtitle, appTitle].filter(Boolean).join(' | ');
const { locationMetadata, spec } = entityMetadata || {};
const lifecycle = spec?.lifecycle;
@@ -157,6 +165,26 @@ export const TechDocsReaderPageHeader = (
const noTdMetadata = !metadataLoading && metadata === undefined;
if (noEntMetadata || noTdMetadata) return null;
const stringEntityRef = stringifyEntityRef(entityRef);
const entityDisplayName =
entityPresentationApi.forEntity(stringEntityRef).snapshot.primaryTitle;
const removeTrailingSlash = (str: string) => str.replace(/\/$/, '');
const normalizeAndSpace = (str: string) =>
str.replace(/-/g, ' ').split(' ').map(capitalize).join(' ');
let techdocsTabTitleItems: string[] = [];
if (path !== '')
techdocsTabTitleItems = removeTrailingSlash(path)
.split('/')
.slice(0, 3)
.map(normalizeAndSpace);
const tabTitleItems = [appTitle, entityDisplayName, ...techdocsTabTitleItems];
const tabTitle = tabTitleItems.join(' | ');
return (
<Header
type="Documentation"